mirror of
https://github.com/YunoHost-Apps/jappix_ynh.git
synced 2024-09-03 19:26:19 +02:00
37142 lines
993 KiB
Text
37142 lines
993 KiB
Text
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the origin JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: dual-licensed under AGPL and MPLv2
|
|
Author: Valérian Saliou
|
|
Last revision: 13/06/13
|
|
|
|
*/
|
|
|
|
// Checks if the URL passed has the same origin than Jappix itself
|
|
function isSameOrigin(url) {
|
|
/* Source: http://stackoverflow.com/questions/9404793/check-if-same-origin-policy-applies */
|
|
|
|
var loc = window.location,
|
|
a = document.createElement('a');
|
|
|
|
a.href = url;
|
|
|
|
return (!a.hostname || (a.hostname == loc.hostname)) &&
|
|
(!a.port || (a.port == loc.port)) &&
|
|
(!a.protocol || (a.protocol == loc.protocol));
|
|
}
|
|
// jXHR.js (JSON-P XHR)
|
|
// v0.1 (c) Kyle Simpson
|
|
// License: MIT
|
|
// modified by gueron Jonathan to work with strophe lib
|
|
// for http://www.iadvize.com
|
|
|
|
(function(global){
|
|
var SETTIMEOUT = global.setTimeout, // for better compression
|
|
doc = global.document,
|
|
callback_counter = 0;
|
|
|
|
global.jXHR = function() {
|
|
var script_url,
|
|
script_loaded,
|
|
jsonp_callback,
|
|
scriptElem,
|
|
publicAPI = null;
|
|
|
|
function removeScript() { try { scriptElem.parentNode.removeChild(scriptElem); } catch (err) { } }
|
|
|
|
function reset() {
|
|
script_loaded = false;
|
|
script_url = "";
|
|
removeScript();
|
|
scriptElem = null;
|
|
fireReadyStateChange(0);
|
|
}
|
|
|
|
function ThrowError(msg) {
|
|
try {
|
|
publicAPI.onerror.call(publicAPI,msg,script_url);
|
|
} catch (err) {
|
|
//throw new Error(msg);
|
|
}
|
|
}
|
|
|
|
function handleScriptLoad() {
|
|
if ((this.readyState && this.readyState!=="complete" && this.readyState!=="loaded") || script_loaded) { return; }
|
|
this.onload = this.onreadystatechange = null; // prevent memory leak
|
|
script_loaded = true;
|
|
if (publicAPI.readyState !== 4) ThrowError("handleScriptLoad: Script failed to load ["+script_url+"].");
|
|
removeScript();
|
|
}
|
|
|
|
function parseXMLString(xmlStr) {
|
|
var xmlDoc = null;
|
|
if(window.DOMParser) {
|
|
var parser = new DOMParser();
|
|
xmlDoc = parser.parseFromString(xmlStr,"text/xml");
|
|
}
|
|
else {
|
|
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
|
|
xmlDoc.async="false";
|
|
xmlDoc.loadXML(xmlStr);
|
|
}
|
|
return xmlDoc;
|
|
}
|
|
|
|
function fireReadyStateChange(rs,args) {
|
|
|
|
args = args || [];
|
|
publicAPI.readyState = rs;
|
|
if (rs == 4) {
|
|
publicAPI.responseText = args[0].reply;
|
|
publicAPI.responseXML = parseXMLString(args[0].reply);
|
|
}
|
|
if (typeof publicAPI.onreadystatechange === "function") publicAPI.onreadystatechange.apply(publicAPI,args);
|
|
}
|
|
|
|
publicAPI = {
|
|
onerror:null,
|
|
onreadystatechange:null,
|
|
readyState:0,
|
|
status:200,
|
|
responseBody: null,
|
|
responseText: null,
|
|
responseXML: null,
|
|
open:function(method,url){
|
|
reset();
|
|
var internal_callback = "cb"+(callback_counter++);
|
|
(function(icb){
|
|
global.jXHR[icb] = function() {
|
|
try { fireReadyStateChange.call(publicAPI,4,arguments); }
|
|
catch(err) {
|
|
publicAPI.readyState = -1;
|
|
ThrowError("Script failed to run ["+script_url+"].");
|
|
}
|
|
global.jXHR[icb] = null;
|
|
};
|
|
})(internal_callback);
|
|
script_url = url + '?callback=?jXHR&data=';
|
|
script_url = script_url.replace(/=\?jXHR/,"=jXHR."+internal_callback);
|
|
fireReadyStateChange(1);
|
|
},
|
|
send:function(data){
|
|
script_url = script_url + encodeURIComponent(data);
|
|
SETTIMEOUT(function(){
|
|
scriptElem = doc.createElement("script");
|
|
scriptElem.setAttribute("type","text/javascript");
|
|
scriptElem.onload = scriptElem.onreadystatechange = function(){handleScriptLoad.call(scriptElem);};
|
|
scriptElem.setAttribute("src",script_url);
|
|
doc.getElementsByTagName("head")[0].appendChild(scriptElem);
|
|
},0);
|
|
fireReadyStateChange(2);
|
|
},
|
|
abort:function(){},
|
|
setRequestHeader:function(){}, // noop
|
|
getResponseHeader:function(){return "";}, // basically noop
|
|
getAllResponseHeaders:function(){return [];} // ditto
|
|
};
|
|
|
|
reset();
|
|
|
|
return publicAPI;
|
|
};
|
|
})(window);
|
|
|
|
/*!
|
|
* jQuery JavaScript Library v1.10.2
|
|
* http://jquery.com/
|
|
*
|
|
* Includes Sizzle.js
|
|
* http://sizzlejs.com/
|
|
*
|
|
* Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
|
|
* Released under the MIT license
|
|
* http://jquery.org/license
|
|
*
|
|
* Date: 2013-07-03T13:48Z
|
|
*/
|
|
(function( window, undefined ) {
|
|
|
|
// Can't do this because several apps including ASP.NET trace
|
|
// the stack via arguments.caller.callee and Firefox dies if
|
|
// you try to trace through "use strict" call chains. (#13335)
|
|
// Support: Firefox 18+
|
|
//"use strict";
|
|
var
|
|
// The deferred used on DOM ready
|
|
readyList,
|
|
|
|
// A central reference to the root jQuery(document)
|
|
rootjQuery,
|
|
|
|
// Support: IE<10
|
|
// For `typeof xmlNode.method` instead of `xmlNode.method !== undefined`
|
|
core_strundefined = typeof undefined,
|
|
|
|
// Use the correct document accordingly with window argument (sandbox)
|
|
location = window.location,
|
|
document = window.document,
|
|
docElem = document.documentElement,
|
|
|
|
// Map over jQuery in case of overwrite
|
|
_jQuery = window.jQuery,
|
|
|
|
// Map over the $ in case of overwrite
|
|
_$ = window.$,
|
|
|
|
// [[Class]] -> type pairs
|
|
class2type = {},
|
|
|
|
// List of deleted data cache ids, so we can reuse them
|
|
core_deletedIds = [],
|
|
|
|
core_version = "1.10.2",
|
|
|
|
// Save a reference to some core methods
|
|
core_concat = core_deletedIds.concat,
|
|
core_push = core_deletedIds.push,
|
|
core_slice = core_deletedIds.slice,
|
|
core_indexOf = core_deletedIds.indexOf,
|
|
core_toString = class2type.toString,
|
|
core_hasOwn = class2type.hasOwnProperty,
|
|
core_trim = core_version.trim,
|
|
|
|
// Define a local copy of jQuery
|
|
jQuery = function( selector, context ) {
|
|
// The jQuery object is actually just the init constructor 'enhanced'
|
|
return new jQuery.fn.init( selector, context, rootjQuery );
|
|
},
|
|
|
|
// Used for matching numbers
|
|
core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,
|
|
|
|
// Used for splitting on whitespace
|
|
core_rnotwhite = /\S+/g,
|
|
|
|
// Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
|
|
rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
|
|
|
|
// A simple way to check for HTML strings
|
|
// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
|
|
// Strict HTML recognition (#11290: must start with <)
|
|
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
|
|
|
|
// Match a standalone tag
|
|
rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
|
|
|
|
// JSON RegExp
|
|
rvalidchars = /^[\],:{}\s]*$/,
|
|
rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
|
|
rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
|
|
rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,
|
|
|
|
// Matches dashed string for camelizing
|
|
rmsPrefix = /^-ms-/,
|
|
rdashAlpha = /-([\da-z])/gi,
|
|
|
|
// Used by jQuery.camelCase as callback to replace()
|
|
fcamelCase = function( all, letter ) {
|
|
return letter.toUpperCase();
|
|
},
|
|
|
|
// The ready event handler
|
|
completed = function( event ) {
|
|
|
|
// readyState === "complete" is good enough for us to call the dom ready in oldIE
|
|
if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) {
|
|
detach();
|
|
jQuery.ready();
|
|
}
|
|
},
|
|
// Clean-up method for dom ready events
|
|
detach = function() {
|
|
if ( document.addEventListener ) {
|
|
document.removeEventListener( "DOMContentLoaded", completed, false );
|
|
window.removeEventListener( "load", completed, false );
|
|
|
|
} else {
|
|
document.detachEvent( "onreadystatechange", completed );
|
|
window.detachEvent( "onload", completed );
|
|
}
|
|
};
|
|
|
|
jQuery.fn = jQuery.prototype = {
|
|
// The current version of jQuery being used
|
|
jquery: core_version,
|
|
|
|
constructor: jQuery,
|
|
init: function( selector, context, rootjQuery ) {
|
|
var match, elem;
|
|
|
|
// HANDLE: $(""), $(null), $(undefined), $(false)
|
|
if ( !selector ) {
|
|
return this;
|
|
}
|
|
|
|
// Handle HTML strings
|
|
if ( typeof selector === "string" ) {
|
|
if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
|
|
// Assume that strings that start and end with <> are HTML and skip the regex check
|
|
match = [ null, selector, null ];
|
|
|
|
} else {
|
|
match = rquickExpr.exec( selector );
|
|
}
|
|
|
|
// Match html or make sure no context is specified for #id
|
|
if ( match && (match[1] || !context) ) {
|
|
|
|
// HANDLE: $(html) -> $(array)
|
|
if ( match[1] ) {
|
|
context = context instanceof jQuery ? context[0] : context;
|
|
|
|
// scripts is true for back-compat
|
|
jQuery.merge( this, jQuery.parseHTML(
|
|
match[1],
|
|
context && context.nodeType ? context.ownerDocument || context : document,
|
|
true
|
|
) );
|
|
|
|
// HANDLE: $(html, props)
|
|
if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
|
|
for ( match in context ) {
|
|
// Properties of context are called as methods if possible
|
|
if ( jQuery.isFunction( this[ match ] ) ) {
|
|
this[ match ]( context[ match ] );
|
|
|
|
// ...and otherwise set as attributes
|
|
} else {
|
|
this.attr( match, context[ match ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
|
|
// HANDLE: $(#id)
|
|
} else {
|
|
elem = document.getElementById( match[2] );
|
|
|
|
// Check parentNode to catch when Blackberry 4.6 returns
|
|
// nodes that are no longer in the document #6963
|
|
if ( elem && elem.parentNode ) {
|
|
// Handle the case where IE and Opera return items
|
|
// by name instead of ID
|
|
if ( elem.id !== match[2] ) {
|
|
return rootjQuery.find( selector );
|
|
}
|
|
|
|
// Otherwise, we inject the element directly into the jQuery object
|
|
this.length = 1;
|
|
this[0] = elem;
|
|
}
|
|
|
|
this.context = document;
|
|
this.selector = selector;
|
|
return this;
|
|
}
|
|
|
|
// HANDLE: $(expr, $(...))
|
|
} else if ( !context || context.jquery ) {
|
|
return ( context || rootjQuery ).find( selector );
|
|
|
|
// HANDLE: $(expr, context)
|
|
// (which is just equivalent to: $(context).find(expr)
|
|
} else {
|
|
return this.constructor( context ).find( selector );
|
|
}
|
|
|
|
// HANDLE: $(DOMElement)
|
|
} else if ( selector.nodeType ) {
|
|
this.context = this[0] = selector;
|
|
this.length = 1;
|
|
return this;
|
|
|
|
// HANDLE: $(function)
|
|
// Shortcut for document ready
|
|
} else if ( jQuery.isFunction( selector ) ) {
|
|
return rootjQuery.ready( selector );
|
|
}
|
|
|
|
if ( selector.selector !== undefined ) {
|
|
this.selector = selector.selector;
|
|
this.context = selector.context;
|
|
}
|
|
|
|
return jQuery.makeArray( selector, this );
|
|
},
|
|
|
|
// Start with an empty selector
|
|
selector: "",
|
|
|
|
// The default length of a jQuery object is 0
|
|
length: 0,
|
|
|
|
toArray: function() {
|
|
return core_slice.call( this );
|
|
},
|
|
|
|
// Get the Nth element in the matched element set OR
|
|
// Get the whole matched element set as a clean array
|
|
get: function( num ) {
|
|
return num == null ?
|
|
|
|
// Return a 'clean' array
|
|
this.toArray() :
|
|
|
|
// Return just the object
|
|
( num < 0 ? this[ this.length + num ] : this[ num ] );
|
|
},
|
|
|
|
// Take an array of elements and push it onto the stack
|
|
// (returning the new matched element set)
|
|
pushStack: function( elems ) {
|
|
|
|
// Build a new jQuery matched element set
|
|
var ret = jQuery.merge( this.constructor(), elems );
|
|
|
|
// Add the old object onto the stack (as a reference)
|
|
ret.prevObject = this;
|
|
ret.context = this.context;
|
|
|
|
// Return the newly-formed element set
|
|
return ret;
|
|
},
|
|
|
|
// Execute a callback for every element in the matched set.
|
|
// (You can seed the arguments with an array of args, but this is
|
|
// only used internally.)
|
|
each: function( callback, args ) {
|
|
return jQuery.each( this, callback, args );
|
|
},
|
|
|
|
ready: function( fn ) {
|
|
// Add the callback
|
|
jQuery.ready.promise().done( fn );
|
|
|
|
return this;
|
|
},
|
|
|
|
slice: function() {
|
|
return this.pushStack( core_slice.apply( this, arguments ) );
|
|
},
|
|
|
|
first: function() {
|
|
return this.eq( 0 );
|
|
},
|
|
|
|
last: function() {
|
|
return this.eq( -1 );
|
|
},
|
|
|
|
eq: function( i ) {
|
|
var len = this.length,
|
|
j = +i + ( i < 0 ? len : 0 );
|
|
return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
|
|
},
|
|
|
|
map: function( callback ) {
|
|
return this.pushStack( jQuery.map(this, function( elem, i ) {
|
|
return callback.call( elem, i, elem );
|
|
}));
|
|
},
|
|
|
|
end: function() {
|
|
return this.prevObject || this.constructor(null);
|
|
},
|
|
|
|
// For internal use only.
|
|
// Behaves like an Array's method, not like a jQuery method.
|
|
push: core_push,
|
|
sort: [].sort,
|
|
splice: [].splice
|
|
};
|
|
|
|
// Give the init function the jQuery prototype for later instantiation
|
|
jQuery.fn.init.prototype = jQuery.fn;
|
|
|
|
jQuery.extend = jQuery.fn.extend = function() {
|
|
var src, copyIsArray, copy, name, options, clone,
|
|
target = arguments[0] || {},
|
|
i = 1,
|
|
length = arguments.length,
|
|
deep = false;
|
|
|
|
// Handle a deep copy situation
|
|
if ( typeof target === "boolean" ) {
|
|
deep = target;
|
|
target = arguments[1] || {};
|
|
// skip the boolean and the target
|
|
i = 2;
|
|
}
|
|
|
|
// Handle case when target is a string or something (possible in deep copy)
|
|
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
|
|
target = {};
|
|
}
|
|
|
|
// extend jQuery itself if only one argument is passed
|
|
if ( length === i ) {
|
|
target = this;
|
|
--i;
|
|
}
|
|
|
|
for ( ; i < length; i++ ) {
|
|
// Only deal with non-null/undefined values
|
|
if ( (options = arguments[ i ]) != null ) {
|
|
// Extend the base object
|
|
for ( name in options ) {
|
|
src = target[ name ];
|
|
copy = options[ name ];
|
|
|
|
// Prevent never-ending loop
|
|
if ( target === copy ) {
|
|
continue;
|
|
}
|
|
|
|
// Recurse if we're merging plain objects or arrays
|
|
if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
|
|
if ( copyIsArray ) {
|
|
copyIsArray = false;
|
|
clone = src && jQuery.isArray(src) ? src : [];
|
|
|
|
} else {
|
|
clone = src && jQuery.isPlainObject(src) ? src : {};
|
|
}
|
|
|
|
// Never move original objects, clone them
|
|
target[ name ] = jQuery.extend( deep, clone, copy );
|
|
|
|
// Don't bring in undefined values
|
|
} else if ( copy !== undefined ) {
|
|
target[ name ] = copy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the modified object
|
|
return target;
|
|
};
|
|
|
|
jQuery.extend({
|
|
// Unique for each copy of jQuery on the page
|
|
// Non-digits removed to match rinlinejQuery
|
|
expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
|
|
|
|
noConflict: function( deep ) {
|
|
if ( window.$ === jQuery ) {
|
|
window.$ = _$;
|
|
}
|
|
|
|
if ( deep && window.jQuery === jQuery ) {
|
|
window.jQuery = _jQuery;
|
|
}
|
|
|
|
return jQuery;
|
|
},
|
|
|
|
// Is the DOM ready to be used? Set to true once it occurs.
|
|
isReady: false,
|
|
|
|
// A counter to track how many items to wait for before
|
|
// the ready event fires. See #6781
|
|
readyWait: 1,
|
|
|
|
// Hold (or release) the ready event
|
|
holdReady: function( hold ) {
|
|
if ( hold ) {
|
|
jQuery.readyWait++;
|
|
} else {
|
|
jQuery.ready( true );
|
|
}
|
|
},
|
|
|
|
// Handle when the DOM is ready
|
|
ready: function( wait ) {
|
|
|
|
// Abort if there are pending holds or we're already ready
|
|
if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
|
|
return;
|
|
}
|
|
|
|
// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
|
|
if ( !document.body ) {
|
|
return setTimeout( jQuery.ready );
|
|
}
|
|
|
|
// Remember that the DOM is ready
|
|
jQuery.isReady = true;
|
|
|
|
// If a normal DOM Ready event fired, decrement, and wait if need be
|
|
if ( wait !== true && --jQuery.readyWait > 0 ) {
|
|
return;
|
|
}
|
|
|
|
// If there are functions bound, to execute
|
|
readyList.resolveWith( document, [ jQuery ] );
|
|
|
|
// Trigger any bound ready events
|
|
if ( jQuery.fn.trigger ) {
|
|
jQuery( document ).trigger("ready").off("ready");
|
|
}
|
|
},
|
|
|
|
// See test/unit/core.js for details concerning isFunction.
|
|
// Since version 1.3, DOM methods and functions like alert
|
|
// aren't supported. They return false on IE (#2968).
|
|
isFunction: function( obj ) {
|
|
return jQuery.type(obj) === "function";
|
|
},
|
|
|
|
isArray: Array.isArray || function( obj ) {
|
|
return jQuery.type(obj) === "array";
|
|
},
|
|
|
|
isWindow: function( obj ) {
|
|
/* jshint eqeqeq: false */
|
|
return obj != null && obj == obj.window;
|
|
},
|
|
|
|
isNumeric: function( obj ) {
|
|
return !isNaN( parseFloat(obj) ) && isFinite( obj );
|
|
},
|
|
|
|
type: function( obj ) {
|
|
if ( obj == null ) {
|
|
return String( obj );
|
|
}
|
|
return typeof obj === "object" || typeof obj === "function" ?
|
|
class2type[ core_toString.call(obj) ] || "object" :
|
|
typeof obj;
|
|
},
|
|
|
|
isPlainObject: function( obj ) {
|
|
var key;
|
|
|
|
// Must be an Object.
|
|
// Because of IE, we also have to check the presence of the constructor property.
|
|
// Make sure that DOM nodes and window objects don't pass through, as well
|
|
if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Not own constructor property must be Object
|
|
if ( obj.constructor &&
|
|
!core_hasOwn.call(obj, "constructor") &&
|
|
!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
|
|
return false;
|
|
}
|
|
} catch ( e ) {
|
|
// IE8,9 Will throw exceptions on certain host objects #9897
|
|
return false;
|
|
}
|
|
|
|
// Support: IE<9
|
|
// Handle iteration over inherited properties before own properties.
|
|
if ( jQuery.support.ownLast ) {
|
|
for ( key in obj ) {
|
|
return core_hasOwn.call( obj, key );
|
|
}
|
|
}
|
|
|
|
// Own properties are enumerated firstly, so to speed up,
|
|
// if last one is own, then all properties are own.
|
|
for ( key in obj ) {}
|
|
|
|
return key === undefined || core_hasOwn.call( obj, key );
|
|
},
|
|
|
|
isEmptyObject: function( obj ) {
|
|
var name;
|
|
for ( name in obj ) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
error: function( msg ) {
|
|
throw new Error( msg );
|
|
},
|
|
|
|
// data: string of html
|
|
// context (optional): If specified, the fragment will be created in this context, defaults to document
|
|
// keepScripts (optional): If true, will include scripts passed in the html string
|
|
parseHTML: function( data, context, keepScripts ) {
|
|
if ( !data || typeof data !== "string" ) {
|
|
return null;
|
|
}
|
|
if ( typeof context === "boolean" ) {
|
|
keepScripts = context;
|
|
context = false;
|
|
}
|
|
context = context || document;
|
|
|
|
var parsed = rsingleTag.exec( data ),
|
|
scripts = !keepScripts && [];
|
|
|
|
// Single tag
|
|
if ( parsed ) {
|
|
return [ context.createElement( parsed[1] ) ];
|
|
}
|
|
|
|
parsed = jQuery.buildFragment( [ data ], context, scripts );
|
|
if ( scripts ) {
|
|
jQuery( scripts ).remove();
|
|
}
|
|
return jQuery.merge( [], parsed.childNodes );
|
|
},
|
|
|
|
parseJSON: function( data ) {
|
|
// Attempt to parse using the native JSON parser first
|
|
if ( window.JSON && window.JSON.parse ) {
|
|
return window.JSON.parse( data );
|
|
}
|
|
|
|
if ( data === null ) {
|
|
return data;
|
|
}
|
|
|
|
if ( typeof data === "string" ) {
|
|
|
|
// Make sure leading/trailing whitespace is removed (IE can't handle it)
|
|
data = jQuery.trim( data );
|
|
|
|
if ( data ) {
|
|
// Make sure the incoming data is actual JSON
|
|
// Logic borrowed from http://json.org/json2.js
|
|
if ( rvalidchars.test( data.replace( rvalidescape, "@" )
|
|
.replace( rvalidtokens, "]" )
|
|
.replace( rvalidbraces, "")) ) {
|
|
|
|
return ( new Function( "return " + data ) )();
|
|
}
|
|
}
|
|
}
|
|
|
|
jQuery.error( "Invalid JSON: " + data );
|
|
},
|
|
|
|
// Cross-browser xml parsing
|
|
parseXML: function( data ) {
|
|
var xml, tmp;
|
|
if ( !data || typeof data !== "string" ) {
|
|
return null;
|
|
}
|
|
try {
|
|
if ( window.DOMParser ) { // Standard
|
|
tmp = new DOMParser();
|
|
xml = tmp.parseFromString( data , "text/xml" );
|
|
} else { // IE
|
|
xml = new ActiveXObject( "Microsoft.XMLDOM" );
|
|
xml.async = "false";
|
|
xml.loadXML( data );
|
|
}
|
|
} catch( e ) {
|
|
xml = undefined;
|
|
}
|
|
if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
|
|
jQuery.error( "Invalid XML: " + data );
|
|
}
|
|
return xml;
|
|
},
|
|
|
|
noop: function() {},
|
|
|
|
// Evaluates a script in a global context
|
|
// Workarounds based on findings by Jim Driscoll
|
|
// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
|
|
globalEval: function( data ) {
|
|
if ( data && jQuery.trim( data ) ) {
|
|
// We use execScript on Internet Explorer
|
|
// We use an anonymous function so that context is window
|
|
// rather than jQuery in Firefox
|
|
( window.execScript || function( data ) {
|
|
window[ "eval" ].call( window, data );
|
|
} )( data );
|
|
}
|
|
},
|
|
|
|
// Convert dashed to camelCase; used by the css and data modules
|
|
// Microsoft forgot to hump their vendor prefix (#9572)
|
|
camelCase: function( string ) {
|
|
return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
|
|
},
|
|
|
|
nodeName: function( elem, name ) {
|
|
return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
|
|
},
|
|
|
|
// args is for internal usage only
|
|
each: function( obj, callback, args ) {
|
|
var value,
|
|
i = 0,
|
|
length = obj.length,
|
|
isArray = isArraylike( obj );
|
|
|
|
if ( args ) {
|
|
if ( isArray ) {
|
|
for ( ; i < length; i++ ) {
|
|
value = callback.apply( obj[ i ], args );
|
|
|
|
if ( value === false ) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for ( i in obj ) {
|
|
value = callback.apply( obj[ i ], args );
|
|
|
|
if ( value === false ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// A special, fast, case for the most common use of each
|
|
} else {
|
|
if ( isArray ) {
|
|
for ( ; i < length; i++ ) {
|
|
value = callback.call( obj[ i ], i, obj[ i ] );
|
|
|
|
if ( value === false ) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for ( i in obj ) {
|
|
value = callback.call( obj[ i ], i, obj[ i ] );
|
|
|
|
if ( value === false ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
|
|
// Use native String.trim function wherever possible
|
|
trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
|
|
function( text ) {
|
|
return text == null ?
|
|
"" :
|
|
core_trim.call( text );
|
|
} :
|
|
|
|
// Otherwise use our own trimming functionality
|
|
function( text ) {
|
|
return text == null ?
|
|
"" :
|
|
( text + "" ).replace( rtrim, "" );
|
|
},
|
|
|
|
// results is for internal usage only
|
|
makeArray: function( arr, results ) {
|
|
var ret = results || [];
|
|
|
|
if ( arr != null ) {
|
|
if ( isArraylike( Object(arr) ) ) {
|
|
jQuery.merge( ret,
|
|
typeof arr === "string" ?
|
|
[ arr ] : arr
|
|
);
|
|
} else {
|
|
core_push.call( ret, arr );
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
},
|
|
|
|
inArray: function( elem, arr, i ) {
|
|
var len;
|
|
|
|
if ( arr ) {
|
|
if ( core_indexOf ) {
|
|
return core_indexOf.call( arr, elem, i );
|
|
}
|
|
|
|
len = arr.length;
|
|
i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
|
|
|
|
for ( ; i < len; i++ ) {
|
|
// Skip accessing in sparse arrays
|
|
if ( i in arr && arr[ i ] === elem ) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
},
|
|
|
|
merge: function( first, second ) {
|
|
var l = second.length,
|
|
i = first.length,
|
|
j = 0;
|
|
|
|
if ( typeof l === "number" ) {
|
|
for ( ; j < l; j++ ) {
|
|
first[ i++ ] = second[ j ];
|
|
}
|
|
} else {
|
|
while ( second[j] !== undefined ) {
|
|
first[ i++ ] = second[ j++ ];
|
|
}
|
|
}
|
|
|
|
first.length = i;
|
|
|
|
return first;
|
|
},
|
|
|
|
grep: function( elems, callback, inv ) {
|
|
var retVal,
|
|
ret = [],
|
|
i = 0,
|
|
length = elems.length;
|
|
inv = !!inv;
|
|
|
|
// Go through the array, only saving the items
|
|
// that pass the validator function
|
|
for ( ; i < length; i++ ) {
|
|
retVal = !!callback( elems[ i ], i );
|
|
if ( inv !== retVal ) {
|
|
ret.push( elems[ i ] );
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
},
|
|
|
|
// arg is for internal usage only
|
|
map: function( elems, callback, arg ) {
|
|
var value,
|
|
i = 0,
|
|
length = elems.length,
|
|
isArray = isArraylike( elems ),
|
|
ret = [];
|
|
|
|
// Go through the array, translating each of the items to their
|
|
if ( isArray ) {
|
|
for ( ; i < length; i++ ) {
|
|
value = callback( elems[ i ], i, arg );
|
|
|
|
if ( value != null ) {
|
|
ret[ ret.length ] = value;
|
|
}
|
|
}
|
|
|
|
// Go through every key on the object,
|
|
} else {
|
|
for ( i in elems ) {
|
|
value = callback( elems[ i ], i, arg );
|
|
|
|
if ( value != null ) {
|
|
ret[ ret.length ] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flatten any nested arrays
|
|
return core_concat.apply( [], ret );
|
|
},
|
|
|
|
// A global GUID counter for objects
|
|
guid: 1,
|
|
|
|
// Bind a function to a context, optionally partially applying any
|
|
// arguments.
|
|
proxy: function( fn, context ) {
|
|
var args, proxy, tmp;
|
|
|
|
if ( typeof context === "string" ) {
|
|
tmp = fn[ context ];
|
|
context = fn;
|
|
fn = tmp;
|
|
}
|
|
|
|
// Quick check to determine if target is callable, in the spec
|
|
// this throws a TypeError, but we will just return undefined.
|
|
if ( !jQuery.isFunction( fn ) ) {
|
|
return undefined;
|
|
}
|
|
|
|
// Simulated bind
|
|
args = core_slice.call( arguments, 2 );
|
|
proxy = function() {
|
|
return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
|
|
};
|
|
|
|
// Set the guid of unique handler to the same of original handler, so it can be removed
|
|
proxy.guid = fn.guid = fn.guid || jQuery.guid++;
|
|
|
|
return proxy;
|
|
},
|
|
|
|
// Multifunctional method to get and set values of a collection
|
|
// The value/s can optionally be executed if it's a function
|
|
access: function( elems, fn, key, value, chainable, emptyGet, raw ) {
|
|
var i = 0,
|
|
length = elems.length,
|
|
bulk = key == null;
|
|
|
|
// Sets many values
|
|
if ( jQuery.type( key ) === "object" ) {
|
|
chainable = true;
|
|
for ( i in key ) {
|
|
jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
|
|
}
|
|
|
|
// Sets one value
|
|
} else if ( value !== undefined ) {
|
|
chainable = true;
|
|
|
|
if ( !jQuery.isFunction( value ) ) {
|
|
raw = true;
|
|
}
|
|
|
|
if ( bulk ) {
|
|
// Bulk operations run against the entire set
|
|
if ( raw ) {
|
|
fn.call( elems, value );
|
|
fn = null;
|
|
|
|
// ...except when executing function values
|
|
} else {
|
|
bulk = fn;
|
|
fn = function( elem, key, value ) {
|
|
return bulk.call( jQuery( elem ), value );
|
|
};
|
|
}
|
|
}
|
|
|
|
if ( fn ) {
|
|
for ( ; i < length; i++ ) {
|
|
fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
return chainable ?
|
|
elems :
|
|
|
|
// Gets
|
|
bulk ?
|
|
fn.call( elems ) :
|
|
length ? fn( elems[0], key ) : emptyGet;
|
|
},
|
|
|
|
now: function() {
|
|
return ( new Date() ).getTime();
|
|
},
|
|
|
|
// A method for quickly swapping in/out CSS properties to get correct calculations.
|
|
// Note: this method belongs to the css module but it's needed here for the support module.
|
|
// If support gets modularized, this method should be moved back to the css module.
|
|
swap: function( elem, options, callback, args ) {
|
|
var ret, name,
|
|
old = {};
|
|
|
|
// Remember the old values, and insert the new ones
|
|
for ( name in options ) {
|
|
old[ name ] = elem.style[ name ];
|
|
elem.style[ name ] = options[ name ];
|
|
}
|
|
|
|
ret = callback.apply( elem, args || [] );
|
|
|
|
// Revert the old values
|
|
for ( name in options ) {
|
|
elem.style[ name ] = old[ name ];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
});
|
|
|
|
jQuery.ready.promise = function( obj ) {
|
|
if ( !readyList ) {
|
|
|
|
readyList = jQuery.Deferred();
|
|
|
|
// Catch cases where $(document).ready() is called after the browser event has already occurred.
|
|
// we once tried to use readyState "interactive" here, but it caused issues like the one
|
|
// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
|
|
if ( document.readyState === "complete" ) {
|
|
// Handle it asynchronously to allow scripts the opportunity to delay ready
|
|
setTimeout( jQuery.ready );
|
|
|
|
// Standards-based browsers support DOMContentLoaded
|
|
} else if ( document.addEventListener ) {
|
|
// Use the handy event callback
|
|
document.addEventListener( "DOMContentLoaded", completed, false );
|
|
|
|
// A fallback to window.onload, that will always work
|
|
window.addEventListener( "load", completed, false );
|
|
|
|
// If IE event model is used
|
|
} else {
|
|
// Ensure firing before onload, maybe late but safe also for iframes
|
|
document.attachEvent( "onreadystatechange", completed );
|
|
|
|
// A fallback to window.onload, that will always work
|
|
window.attachEvent( "onload", completed );
|
|
|
|
// If IE and not a frame
|
|
// continually check to see if the document is ready
|
|
var top = false;
|
|
|
|
try {
|
|
top = window.frameElement == null && document.documentElement;
|
|
} catch(e) {}
|
|
|
|
if ( top && top.doScroll ) {
|
|
(function doScrollCheck() {
|
|
if ( !jQuery.isReady ) {
|
|
|
|
try {
|
|
// Use the trick by Diego Perini
|
|
// http://javascript.nwbox.com/IEContentLoaded/
|
|
top.doScroll("left");
|
|
} catch(e) {
|
|
return setTimeout( doScrollCheck, 50 );
|
|
}
|
|
|
|
// detach all dom ready events
|
|
detach();
|
|
|
|
// and execute any waiting functions
|
|
jQuery.ready();
|
|
}
|
|
})();
|
|
}
|
|
}
|
|
}
|
|
return readyList.promise( obj );
|
|
};
|
|
|
|
// Populate the class2type map
|
|
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
|
|
class2type[ "[object " + name + "]" ] = name.toLowerCase();
|
|
});
|
|
|
|
function isArraylike( obj ) {
|
|
var length = obj.length,
|
|
type = jQuery.type( obj );
|
|
|
|
if ( jQuery.isWindow( obj ) ) {
|
|
return false;
|
|
}
|
|
|
|
if ( obj.nodeType === 1 && length ) {
|
|
return true;
|
|
}
|
|
|
|
return type === "array" || type !== "function" &&
|
|
( length === 0 ||
|
|
typeof length === "number" && length > 0 && ( length - 1 ) in obj );
|
|
}
|
|
|
|
// All jQuery objects should point back to these
|
|
rootjQuery = jQuery(document);
|
|
/*!
|
|
* Sizzle CSS Selector Engine v1.10.2
|
|
* http://sizzlejs.com/
|
|
*
|
|
* Copyright 2013 jQuery Foundation, Inc. and other contributors
|
|
* Released under the MIT license
|
|
* http://jquery.org/license
|
|
*
|
|
* Date: 2013-07-03
|
|
*/
|
|
(function( window, undefined ) {
|
|
|
|
var i,
|
|
support,
|
|
cachedruns,
|
|
Expr,
|
|
getText,
|
|
isXML,
|
|
compile,
|
|
outermostContext,
|
|
sortInput,
|
|
|
|
// Local document vars
|
|
setDocument,
|
|
document,
|
|
docElem,
|
|
documentIsHTML,
|
|
rbuggyQSA,
|
|
rbuggyMatches,
|
|
matches,
|
|
contains,
|
|
|
|
// Instance-specific data
|
|
expando = "sizzle" + -(new Date()),
|
|
preferredDoc = window.document,
|
|
dirruns = 0,
|
|
done = 0,
|
|
classCache = createCache(),
|
|
tokenCache = createCache(),
|
|
compilerCache = createCache(),
|
|
hasDuplicate = false,
|
|
sortOrder = function( a, b ) {
|
|
if ( a === b ) {
|
|
hasDuplicate = true;
|
|
return 0;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
// General-purpose constants
|
|
strundefined = typeof undefined,
|
|
MAX_NEGATIVE = 1 << 31,
|
|
|
|
// Instance methods
|
|
hasOwn = ({}).hasOwnProperty,
|
|
arr = [],
|
|
pop = arr.pop,
|
|
push_native = arr.push,
|
|
push = arr.push,
|
|
slice = arr.slice,
|
|
// Use a stripped-down indexOf if we can't use a native one
|
|
indexOf = arr.indexOf || function( elem ) {
|
|
var i = 0,
|
|
len = this.length;
|
|
for ( ; i < len; i++ ) {
|
|
if ( this[i] === elem ) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
|
|
|
|
// Regular expressions
|
|
|
|
// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
|
|
whitespace = "[\\x20\\t\\r\\n\\f]",
|
|
// http://www.w3.org/TR/css3-syntax/#characters
|
|
characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
|
|
|
|
// Loosely modeled on CSS identifier characters
|
|
// An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
|
|
// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
|
|
identifier = characterEncoding.replace( "w", "w#" ),
|
|
|
|
// Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
|
|
attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
|
|
"*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
|
|
|
|
// Prefer arguments quoted,
|
|
// then not containing pseudos/brackets,
|
|
// then attribute selectors/non-parenthetical expressions,
|
|
// then anything else
|
|
// These preferences are here to reduce the number of selectors
|
|
// needing tokenize in the PSEUDO preFilter
|
|
pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)",
|
|
|
|
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
|
|
rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
|
|
|
|
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
|
|
rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
|
|
|
|
rsibling = new RegExp( whitespace + "*[+~]" ),
|
|
rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ),
|
|
|
|
rpseudo = new RegExp( pseudos ),
|
|
ridentifier = new RegExp( "^" + identifier + "$" ),
|
|
|
|
matchExpr = {
|
|
"ID": new RegExp( "^#(" + characterEncoding + ")" ),
|
|
"CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
|
|
"TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
|
|
"ATTR": new RegExp( "^" + attributes ),
|
|
"PSEUDO": new RegExp( "^" + pseudos ),
|
|
"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
|
|
"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
|
|
"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
|
|
"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
|
|
// For use in libraries implementing .is()
|
|
// We use this for POS matching in `select`
|
|
"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
|
|
whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
|
|
},
|
|
|
|
rnative = /^[^{]+\{\s*\[native \w/,
|
|
|
|
// Easily-parseable/retrievable ID or TAG or CLASS selectors
|
|
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
|
|
|
|
rinputs = /^(?:input|select|textarea|button)$/i,
|
|
rheader = /^h\d$/i,
|
|
|
|
rescape = /'|\\/g,
|
|
|
|
// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
|
|
runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
|
|
funescape = function( _, escaped, escapedWhitespace ) {
|
|
var high = "0x" + escaped - 0x10000;
|
|
// NaN means non-codepoint
|
|
// Support: Firefox
|
|
// Workaround erroneous numeric interpretation of +"0x"
|
|
return high !== high || escapedWhitespace ?
|
|
escaped :
|
|
// BMP codepoint
|
|
high < 0 ?
|
|
String.fromCharCode( high + 0x10000 ) :
|
|
// Supplemental Plane codepoint (surrogate pair)
|
|
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
|
|
};
|
|
|
|
// Optimize for push.apply( _, NodeList )
|
|
try {
|
|
push.apply(
|
|
(arr = slice.call( preferredDoc.childNodes )),
|
|
preferredDoc.childNodes
|
|
);
|
|
// Support: Android<4.0
|
|
// Detect silently failing push.apply
|
|
arr[ preferredDoc.childNodes.length ].nodeType;
|
|
} catch ( e ) {
|
|
push = { apply: arr.length ?
|
|
|
|
// Leverage slice if possible
|
|
function( target, els ) {
|
|
push_native.apply( target, slice.call(els) );
|
|
} :
|
|
|
|
// Support: IE<9
|
|
// Otherwise append directly
|
|
function( target, els ) {
|
|
var j = target.length,
|
|
i = 0;
|
|
// Can't trust NodeList.length
|
|
while ( (target[j++] = els[i++]) ) {}
|
|
target.length = j - 1;
|
|
}
|
|
};
|
|
}
|
|
|
|
function Sizzle( selector, context, results, seed ) {
|
|
var match, elem, m, nodeType,
|
|
// QSA vars
|
|
i, groups, old, nid, newContext, newSelector;
|
|
|
|
if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
|
|
setDocument( context );
|
|
}
|
|
|
|
context = context || document;
|
|
results = results || [];
|
|
|
|
if ( !selector || typeof selector !== "string" ) {
|
|
return results;
|
|
}
|
|
|
|
if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
|
|
return [];
|
|
}
|
|
|
|
if ( documentIsHTML && !seed ) {
|
|
|
|
// Shortcuts
|
|
if ( (match = rquickExpr.exec( selector )) ) {
|
|
// Speed-up: Sizzle("#ID")
|
|
if ( (m = match[1]) ) {
|
|
if ( nodeType === 9 ) {
|
|
elem = context.getElementById( m );
|
|
// Check parentNode to catch when Blackberry 4.6 returns
|
|
// nodes that are no longer in the document #6963
|
|
if ( elem && elem.parentNode ) {
|
|
// Handle the case where IE, Opera, and Webkit return items
|
|
// by name instead of ID
|
|
if ( elem.id === m ) {
|
|
results.push( elem );
|
|
return results;
|
|
}
|
|
} else {
|
|
return results;
|
|
}
|
|
} else {
|
|
// Context is not a document
|
|
if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
|
|
contains( context, elem ) && elem.id === m ) {
|
|
results.push( elem );
|
|
return results;
|
|
}
|
|
}
|
|
|
|
// Speed-up: Sizzle("TAG")
|
|
} else if ( match[2] ) {
|
|
push.apply( results, context.getElementsByTagName( selector ) );
|
|
return results;
|
|
|
|
// Speed-up: Sizzle(".CLASS")
|
|
} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
|
|
push.apply( results, context.getElementsByClassName( m ) );
|
|
return results;
|
|
}
|
|
}
|
|
|
|
// QSA path
|
|
if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
|
|
nid = old = expando;
|
|
newContext = context;
|
|
newSelector = nodeType === 9 && selector;
|
|
|
|
// qSA works strangely on Element-rooted queries
|
|
// We can work around this by specifying an extra ID on the root
|
|
// and working up from there (Thanks to Andrew Dupont for the technique)
|
|
// IE 8 doesn't work on object elements
|
|
if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
|
|
groups = tokenize( selector );
|
|
|
|
if ( (old = context.getAttribute("id")) ) {
|
|
nid = old.replace( rescape, "\\$&" );
|
|
} else {
|
|
context.setAttribute( "id", nid );
|
|
}
|
|
nid = "[id='" + nid + "'] ";
|
|
|
|
i = groups.length;
|
|
while ( i-- ) {
|
|
groups[i] = nid + toSelector( groups[i] );
|
|
}
|
|
newContext = rsibling.test( selector ) && context.parentNode || context;
|
|
newSelector = groups.join(",");
|
|
}
|
|
|
|
if ( newSelector ) {
|
|
try {
|
|
push.apply( results,
|
|
newContext.querySelectorAll( newSelector )
|
|
);
|
|
return results;
|
|
} catch(qsaError) {
|
|
} finally {
|
|
if ( !old ) {
|
|
context.removeAttribute("id");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// All others
|
|
return select( selector.replace( rtrim, "$1" ), context, results, seed );
|
|
}
|
|
|
|
/**
|
|
* Create key-value caches of limited size
|
|
* @returns {Function(string, Object)} Returns the Object data after storing it on itself with
|
|
* property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
|
|
* deleting the oldest entry
|
|
*/
|
|
function createCache() {
|
|
var keys = [];
|
|
|
|
function cache( key, value ) {
|
|
// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
|
|
if ( keys.push( key += " " ) > Expr.cacheLength ) {
|
|
// Only keep the most recent entries
|
|
delete cache[ keys.shift() ];
|
|
}
|
|
return (cache[ key ] = value);
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
/**
|
|
* Mark a function for special use by Sizzle
|
|
* @param {Function} fn The function to mark
|
|
*/
|
|
function markFunction( fn ) {
|
|
fn[ expando ] = true;
|
|
return fn;
|
|
}
|
|
|
|
/**
|
|
* Support testing using an element
|
|
* @param {Function} fn Passed the created div and expects a boolean result
|
|
*/
|
|
function assert( fn ) {
|
|
var div = document.createElement("div");
|
|
|
|
try {
|
|
return !!fn( div );
|
|
} catch (e) {
|
|
return false;
|
|
} finally {
|
|
// Remove from its parent by default
|
|
if ( div.parentNode ) {
|
|
div.parentNode.removeChild( div );
|
|
}
|
|
// release memory in IE
|
|
div = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the same handler for all of the specified attrs
|
|
* @param {String} attrs Pipe-separated list of attributes
|
|
* @param {Function} handler The method that will be applied
|
|
*/
|
|
function addHandle( attrs, handler ) {
|
|
var arr = attrs.split("|"),
|
|
i = attrs.length;
|
|
|
|
while ( i-- ) {
|
|
Expr.attrHandle[ arr[i] ] = handler;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks document order of two siblings
|
|
* @param {Element} a
|
|
* @param {Element} b
|
|
* @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
|
|
*/
|
|
function siblingCheck( a, b ) {
|
|
var cur = b && a,
|
|
diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
|
|
( ~b.sourceIndex || MAX_NEGATIVE ) -
|
|
( ~a.sourceIndex || MAX_NEGATIVE );
|
|
|
|
// Use IE sourceIndex if available on both nodes
|
|
if ( diff ) {
|
|
return diff;
|
|
}
|
|
|
|
// Check if b follows a
|
|
if ( cur ) {
|
|
while ( (cur = cur.nextSibling) ) {
|
|
if ( cur === b ) {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return a ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* Returns a function to use in pseudos for input types
|
|
* @param {String} type
|
|
*/
|
|
function createInputPseudo( type ) {
|
|
return function( elem ) {
|
|
var name = elem.nodeName.toLowerCase();
|
|
return name === "input" && elem.type === type;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a function to use in pseudos for buttons
|
|
* @param {String} type
|
|
*/
|
|
function createButtonPseudo( type ) {
|
|
return function( elem ) {
|
|
var name = elem.nodeName.toLowerCase();
|
|
return (name === "input" || name === "button") && elem.type === type;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a function to use in pseudos for positionals
|
|
* @param {Function} fn
|
|
*/
|
|
function createPositionalPseudo( fn ) {
|
|
return markFunction(function( argument ) {
|
|
argument = +argument;
|
|
return markFunction(function( seed, matches ) {
|
|
var j,
|
|
matchIndexes = fn( [], seed.length, argument ),
|
|
i = matchIndexes.length;
|
|
|
|
// Match elements found at the specified indexes
|
|
while ( i-- ) {
|
|
if ( seed[ (j = matchIndexes[i]) ] ) {
|
|
seed[j] = !(matches[j] = seed[j]);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Detect xml
|
|
* @param {Element|Object} elem An element or a document
|
|
*/
|
|
isXML = Sizzle.isXML = function( elem ) {
|
|
// documentElement is verified for cases where it doesn't yet exist
|
|
// (such as loading iframes in IE - #4833)
|
|
var documentElement = elem && (elem.ownerDocument || elem).documentElement;
|
|
return documentElement ? documentElement.nodeName !== "HTML" : false;
|
|
};
|
|
|
|
// Expose support vars for convenience
|
|
support = Sizzle.support = {};
|
|
|
|
/**
|
|
* Sets document-related variables once based on the current document
|
|
* @param {Element|Object} [doc] An element or document object to use to set the document
|
|
* @returns {Object} Returns the current document
|
|
*/
|
|
setDocument = Sizzle.setDocument = function( node ) {
|
|
var doc = node ? node.ownerDocument || node : preferredDoc,
|
|
parent = doc.defaultView;
|
|
|
|
// If no document and documentElement is available, return
|
|
if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
|
|
return document;
|
|
}
|
|
|
|
// Set our document
|
|
document = doc;
|
|
docElem = doc.documentElement;
|
|
|
|
// Support tests
|
|
documentIsHTML = !isXML( doc );
|
|
|
|
// Support: IE>8
|
|
// If iframe document is assigned to "document" variable and if iframe has been reloaded,
|
|
// IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
|
|
// IE6-8 do not support the defaultView property so parent will be undefined
|
|
if ( parent && parent.attachEvent && parent !== parent.top ) {
|
|
parent.attachEvent( "onbeforeunload", function() {
|
|
setDocument();
|
|
});
|
|
}
|
|
|
|
/* Attributes
|
|
---------------------------------------------------------------------- */
|
|
|
|
// Support: IE<8
|
|
// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
|
|
support.attributes = assert(function( div ) {
|
|
div.className = "i";
|
|
return !div.getAttribute("className");
|
|
});
|
|
|
|
/* getElement(s)By*
|
|
---------------------------------------------------------------------- */
|
|
|
|
// Check if getElementsByTagName("*") returns only elements
|
|
support.getElementsByTagName = assert(function( div ) {
|
|
div.appendChild( doc.createComment("") );
|
|
return !div.getElementsByTagName("*").length;
|
|
});
|
|
|
|
// Check if getElementsByClassName can be trusted
|
|
support.getElementsByClassName = assert(function( div ) {
|
|
div.innerHTML = "<div class='a'></div><div class='a i'></div>";
|
|
|
|
// Support: Safari<4
|
|
// Catch class over-caching
|
|
div.firstChild.className = "i";
|
|
// Support: Opera<10
|
|
// Catch gEBCN failure to find non-leading classes
|
|
return div.getElementsByClassName("i").length === 2;
|
|
});
|
|
|
|
// Support: IE<10
|
|
// Check if getElementById returns elements by name
|
|
// The broken getElementById methods don't pick up programatically-set names,
|
|
// so use a roundabout getElementsByName test
|
|
support.getById = assert(function( div ) {
|
|
docElem.appendChild( div ).id = expando;
|
|
return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
|
|
});
|
|
|
|
// ID find and filter
|
|
if ( support.getById ) {
|
|
Expr.find["ID"] = function( id, context ) {
|
|
if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
|
|
var m = context.getElementById( id );
|
|
// Check parentNode to catch when Blackberry 4.6 returns
|
|
// nodes that are no longer in the document #6963
|
|
return m && m.parentNode ? [m] : [];
|
|
}
|
|
};
|
|
Expr.filter["ID"] = function( id ) {
|
|
var attrId = id.replace( runescape, funescape );
|
|
return function( elem ) {
|
|
return elem.getAttribute("id") === attrId;
|
|
};
|
|
};
|
|
} else {
|
|
// Support: IE6/7
|
|
// getElementById is not reliable as a find shortcut
|
|
delete Expr.find["ID"];
|
|
|
|
Expr.filter["ID"] = function( id ) {
|
|
var attrId = id.replace( runescape, funescape );
|
|
return function( elem ) {
|
|
var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
|
|
return node && node.value === attrId;
|
|
};
|
|
};
|
|
}
|
|
|
|
// Tag
|
|
Expr.find["TAG"] = support.getElementsByTagName ?
|
|
function( tag, context ) {
|
|
if ( typeof context.getElementsByTagName !== strundefined ) {
|
|
return context.getElementsByTagName( tag );
|
|
}
|
|
} :
|
|
function( tag, context ) {
|
|
var elem,
|
|
tmp = [],
|
|
i = 0,
|
|
results = context.getElementsByTagName( tag );
|
|
|
|
// Filter out possible comments
|
|
if ( tag === "*" ) {
|
|
while ( (elem = results[i++]) ) {
|
|
if ( elem.nodeType === 1 ) {
|
|
tmp.push( elem );
|
|
}
|
|
}
|
|
|
|
return tmp;
|
|
}
|
|
return results;
|
|
};
|
|
|
|
// Class
|
|
Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
|
|
if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
|
|
return context.getElementsByClassName( className );
|
|
}
|
|
};
|
|
|
|
/* QSA/matchesSelector
|
|
---------------------------------------------------------------------- */
|
|
|
|
// QSA and matchesSelector support
|
|
|
|
// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
|
|
rbuggyMatches = [];
|
|
|
|
// qSa(:focus) reports false when true (Chrome 21)
|
|
// We allow this because of a bug in IE8/9 that throws an error
|
|
// whenever `document.activeElement` is accessed on an iframe
|
|
// So, we allow :focus to pass through QSA all the time to avoid the IE error
|
|
// See http://bugs.jquery.com/ticket/13378
|
|
rbuggyQSA = [];
|
|
|
|
if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
|
|
// Build QSA regex
|
|
// Regex strategy adopted from Diego Perini
|
|
assert(function( div ) {
|
|
// Select is set to empty string on purpose
|
|
// This is to test IE's treatment of not explicitly
|
|
// setting a boolean content attribute,
|
|
// since its presence should be enough
|
|
// http://bugs.jquery.com/ticket/12359
|
|
div.innerHTML = "<select><option selected=''></option></select>";
|
|
|
|
// Support: IE8
|
|
// Boolean attributes and "value" are not treated correctly
|
|
if ( !div.querySelectorAll("[selected]").length ) {
|
|
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
|
|
}
|
|
|
|
// Webkit/Opera - :checked should return selected option elements
|
|
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
|
|
// IE8 throws error here and will not see later tests
|
|
if ( !div.querySelectorAll(":checked").length ) {
|
|
rbuggyQSA.push(":checked");
|
|
}
|
|
});
|
|
|
|
assert(function( div ) {
|
|
|
|
// Support: Opera 10-12/IE8
|
|
// ^= $= *= and empty values
|
|
// Should not select anything
|
|
// Support: Windows 8 Native Apps
|
|
// The type attribute is restricted during .innerHTML assignment
|
|
var input = doc.createElement("input");
|
|
input.setAttribute( "type", "hidden" );
|
|
div.appendChild( input ).setAttribute( "t", "" );
|
|
|
|
if ( div.querySelectorAll("[t^='']").length ) {
|
|
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
|
|
}
|
|
|
|
// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
|
|
// IE8 throws error here and will not see later tests
|
|
if ( !div.querySelectorAll(":enabled").length ) {
|
|
rbuggyQSA.push( ":enabled", ":disabled" );
|
|
}
|
|
|
|
// Opera 10-11 does not throw on post-comma invalid pseudos
|
|
div.querySelectorAll("*,:x");
|
|
rbuggyQSA.push(",.*:");
|
|
});
|
|
}
|
|
|
|
if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector ||
|
|
docElem.mozMatchesSelector ||
|
|
docElem.oMatchesSelector ||
|
|
docElem.msMatchesSelector) )) ) {
|
|
|
|
assert(function( div ) {
|
|
// Check to see if it's possible to do matchesSelector
|
|
// on a disconnected node (IE 9)
|
|
support.disconnectedMatch = matches.call( div, "div" );
|
|
|
|
// This should fail with an exception
|
|
// Gecko does not error, returns false instead
|
|
matches.call( div, "[s!='']:x" );
|
|
rbuggyMatches.push( "!=", pseudos );
|
|
});
|
|
}
|
|
|
|
rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
|
|
rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
|
|
|
|
/* Contains
|
|
---------------------------------------------------------------------- */
|
|
|
|
// Element contains another
|
|
// Purposefully does not implement inclusive descendent
|
|
// As in, an element does not contain itself
|
|
contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ?
|
|
function( a, b ) {
|
|
var adown = a.nodeType === 9 ? a.documentElement : a,
|
|
bup = b && b.parentNode;
|
|
return a === bup || !!( bup && bup.nodeType === 1 && (
|
|
adown.contains ?
|
|
adown.contains( bup ) :
|
|
a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
|
|
));
|
|
} :
|
|
function( a, b ) {
|
|
if ( b ) {
|
|
while ( (b = b.parentNode) ) {
|
|
if ( b === a ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/* Sorting
|
|
---------------------------------------------------------------------- */
|
|
|
|
// Document order sorting
|
|
sortOrder = docElem.compareDocumentPosition ?
|
|
function( a, b ) {
|
|
|
|
// Flag for duplicate removal
|
|
if ( a === b ) {
|
|
hasDuplicate = true;
|
|
return 0;
|
|
}
|
|
|
|
var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b );
|
|
|
|
if ( compare ) {
|
|
// Disconnected nodes
|
|
if ( compare & 1 ||
|
|
(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
|
|
|
|
// Choose the first element that is related to our preferred document
|
|
if ( a === doc || contains(preferredDoc, a) ) {
|
|
return -1;
|
|
}
|
|
if ( b === doc || contains(preferredDoc, b) ) {
|
|
return 1;
|
|
}
|
|
|
|
// Maintain original order
|
|
return sortInput ?
|
|
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
|
|
0;
|
|
}
|
|
|
|
return compare & 4 ? -1 : 1;
|
|
}
|
|
|
|
// Not directly comparable, sort on existence of method
|
|
return a.compareDocumentPosition ? -1 : 1;
|
|
} :
|
|
function( a, b ) {
|
|
var cur,
|
|
i = 0,
|
|
aup = a.parentNode,
|
|
bup = b.parentNode,
|
|
ap = [ a ],
|
|
bp = [ b ];
|
|
|
|
// Exit early if the nodes are identical
|
|
if ( a === b ) {
|
|
hasDuplicate = true;
|
|
return 0;
|
|
|
|
// Parentless nodes are either documents or disconnected
|
|
} else if ( !aup || !bup ) {
|
|
return a === doc ? -1 :
|
|
b === doc ? 1 :
|
|
aup ? -1 :
|
|
bup ? 1 :
|
|
sortInput ?
|
|
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
|
|
0;
|
|
|
|
// If the nodes are siblings, we can do a quick check
|
|
} else if ( aup === bup ) {
|
|
return siblingCheck( a, b );
|
|
}
|
|
|
|
// Otherwise we need full lists of their ancestors for comparison
|
|
cur = a;
|
|
while ( (cur = cur.parentNode) ) {
|
|
ap.unshift( cur );
|
|
}
|
|
cur = b;
|
|
while ( (cur = cur.parentNode) ) {
|
|
bp.unshift( cur );
|
|
}
|
|
|
|
// Walk down the tree looking for a discrepancy
|
|
while ( ap[i] === bp[i] ) {
|
|
i++;
|
|
}
|
|
|
|
return i ?
|
|
// Do a sibling check if the nodes have a common ancestor
|
|
siblingCheck( ap[i], bp[i] ) :
|
|
|
|
// Otherwise nodes in our document sort first
|
|
ap[i] === preferredDoc ? -1 :
|
|
bp[i] === preferredDoc ? 1 :
|
|
0;
|
|
};
|
|
|
|
return doc;
|
|
};
|
|
|
|
Sizzle.matches = function( expr, elements ) {
|
|
return Sizzle( expr, null, null, elements );
|
|
};
|
|
|
|
Sizzle.matchesSelector = function( elem, expr ) {
|
|
// Set document vars if needed
|
|
if ( ( elem.ownerDocument || elem ) !== document ) {
|
|
setDocument( elem );
|
|
}
|
|
|
|
// Make sure that attribute selectors are quoted
|
|
expr = expr.replace( rattributeQuotes, "='$1']" );
|
|
|
|
if ( support.matchesSelector && documentIsHTML &&
|
|
( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
|
|
( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
|
|
|
|
try {
|
|
var ret = matches.call( elem, expr );
|
|
|
|
// IE 9's matchesSelector returns false on disconnected nodes
|
|
if ( ret || support.disconnectedMatch ||
|
|
// As well, disconnected nodes are said to be in a document
|
|
// fragment in IE 9
|
|
elem.document && elem.document.nodeType !== 11 ) {
|
|
return ret;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
return Sizzle( expr, document, null, [elem] ).length > 0;
|
|
};
|
|
|
|
Sizzle.contains = function( context, elem ) {
|
|
// Set document vars if needed
|
|
if ( ( context.ownerDocument || context ) !== document ) {
|
|
setDocument( context );
|
|
}
|
|
return contains( context, elem );
|
|
};
|
|
|
|
Sizzle.attr = function( elem, name ) {
|
|
// Set document vars if needed
|
|
if ( ( elem.ownerDocument || elem ) !== document ) {
|
|
setDocument( elem );
|
|
}
|
|
|
|
var fn = Expr.attrHandle[ name.toLowerCase() ],
|
|
// Don't get fooled by Object.prototype properties (jQuery #13807)
|
|
val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
|
|
fn( elem, name, !documentIsHTML ) :
|
|
undefined;
|
|
|
|
return val === undefined ?
|
|
support.attributes || !documentIsHTML ?
|
|
elem.getAttribute( name ) :
|
|
(val = elem.getAttributeNode(name)) && val.specified ?
|
|
val.value :
|
|
null :
|
|
val;
|
|
};
|
|
|
|
Sizzle.error = function( msg ) {
|
|
throw new Error( "Syntax error, unrecognized expression: " + msg );
|
|
};
|
|
|
|
/**
|
|
* Document sorting and removing duplicates
|
|
* @param {ArrayLike} results
|
|
*/
|
|
Sizzle.uniqueSort = function( results ) {
|
|
var elem,
|
|
duplicates = [],
|
|
j = 0,
|
|
i = 0;
|
|
|
|
// Unless we *know* we can detect duplicates, assume their presence
|
|
hasDuplicate = !support.detectDuplicates;
|
|
sortInput = !support.sortStable && results.slice( 0 );
|
|
results.sort( sortOrder );
|
|
|
|
if ( hasDuplicate ) {
|
|
while ( (elem = results[i++]) ) {
|
|
if ( elem === results[ i ] ) {
|
|
j = duplicates.push( i );
|
|
}
|
|
}
|
|
while ( j-- ) {
|
|
results.splice( duplicates[ j ], 1 );
|
|
}
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
/**
|
|
* Utility function for retrieving the text value of an array of DOM nodes
|
|
* @param {Array|Element} elem
|
|
*/
|
|
getText = Sizzle.getText = function( elem ) {
|
|
var node,
|
|
ret = "",
|
|
i = 0,
|
|
nodeType = elem.nodeType;
|
|
|
|
if ( !nodeType ) {
|
|
// If no nodeType, this is expected to be an array
|
|
for ( ; (node = elem[i]); i++ ) {
|
|
// Do not traverse comment nodes
|
|
ret += getText( node );
|
|
}
|
|
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
|
|
// Use textContent for elements
|
|
// innerText usage removed for consistency of new lines (see #11153)
|
|
if ( typeof elem.textContent === "string" ) {
|
|
return elem.textContent;
|
|
} else {
|
|
// Traverse its children
|
|
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
|
|
ret += getText( elem );
|
|
}
|
|
}
|
|
} else if ( nodeType === 3 || nodeType === 4 ) {
|
|
return elem.nodeValue;
|
|
}
|
|
// Do not include comment or processing instruction nodes
|
|
|
|
return ret;
|
|
};
|
|
|
|
Expr = Sizzle.selectors = {
|
|
|
|
// Can be adjusted by the user
|
|
cacheLength: 50,
|
|
|
|
createPseudo: markFunction,
|
|
|
|
match: matchExpr,
|
|
|
|
attrHandle: {},
|
|
|
|
find: {},
|
|
|
|
relative: {
|
|
">": { dir: "parentNode", first: true },
|
|
" ": { dir: "parentNode" },
|
|
"+": { dir: "previousSibling", first: true },
|
|
"~": { dir: "previousSibling" }
|
|
},
|
|
|
|
preFilter: {
|
|
"ATTR": function( match ) {
|
|
match[1] = match[1].replace( runescape, funescape );
|
|
|
|
// Move the given value to match[3] whether quoted or unquoted
|
|
match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape );
|
|
|
|
if ( match[2] === "~=" ) {
|
|
match[3] = " " + match[3] + " ";
|
|
}
|
|
|
|
return match.slice( 0, 4 );
|
|
},
|
|
|
|
"CHILD": function( match ) {
|
|
/* matches from matchExpr["CHILD"]
|
|
1 type (only|nth|...)
|
|
2 what (child|of-type)
|
|
3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
|
|
4 xn-component of xn+y argument ([+-]?\d*n|)
|
|
5 sign of xn-component
|
|
6 x of xn-component
|
|
7 sign of y-component
|
|
8 y of y-component
|
|
*/
|
|
match[1] = match[1].toLowerCase();
|
|
|
|
if ( match[1].slice( 0, 3 ) === "nth" ) {
|
|
// nth-* requires argument
|
|
if ( !match[3] ) {
|
|
Sizzle.error( match[0] );
|
|
}
|
|
|
|
// numeric x and y parameters for Expr.filter.CHILD
|
|
// remember that false/true cast respectively to 0/1
|
|
match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
|
|
match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
|
|
|
|
// other types prohibit arguments
|
|
} else if ( match[3] ) {
|
|
Sizzle.error( match[0] );
|
|
}
|
|
|
|
return match;
|
|
},
|
|
|
|
"PSEUDO": function( match ) {
|
|
var excess,
|
|
unquoted = !match[5] && match[2];
|
|
|
|
if ( matchExpr["CHILD"].test( match[0] ) ) {
|
|
return null;
|
|
}
|
|
|
|
// Accept quoted arguments as-is
|
|
if ( match[3] && match[4] !== undefined ) {
|
|
match[2] = match[4];
|
|
|
|
// Strip excess characters from unquoted arguments
|
|
} else if ( unquoted && rpseudo.test( unquoted ) &&
|
|
// Get excess from tokenize (recursively)
|
|
(excess = tokenize( unquoted, true )) &&
|
|
// advance to the next closing parenthesis
|
|
(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
|
|
|
|
// excess is a negative index
|
|
match[0] = match[0].slice( 0, excess );
|
|
match[2] = unquoted.slice( 0, excess );
|
|
}
|
|
|
|
// Return only captures needed by the pseudo filter method (type and argument)
|
|
return match.slice( 0, 3 );
|
|
}
|
|
},
|
|
|
|
filter: {
|
|
|
|
"TAG": function( nodeNameSelector ) {
|
|
var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
|
|
return nodeNameSelector === "*" ?
|
|
function() { return true; } :
|
|
function( elem ) {
|
|
return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
|
|
};
|
|
},
|
|
|
|
"CLASS": function( className ) {
|
|
var pattern = classCache[ className + " " ];
|
|
|
|
return pattern ||
|
|
(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
|
|
classCache( className, function( elem ) {
|
|
return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
|
|
});
|
|
},
|
|
|
|
"ATTR": function( name, operator, check ) {
|
|
return function( elem ) {
|
|
var result = Sizzle.attr( elem, name );
|
|
|
|
if ( result == null ) {
|
|
return operator === "!=";
|
|
}
|
|
if ( !operator ) {
|
|
return true;
|
|
}
|
|
|
|
result += "";
|
|
|
|
return operator === "=" ? result === check :
|
|
operator === "!=" ? result !== check :
|
|
operator === "^=" ? check && result.indexOf( check ) === 0 :
|
|
operator === "*=" ? check && result.indexOf( check ) > -1 :
|
|
operator === "$=" ? check && result.slice( -check.length ) === check :
|
|
operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
|
|
operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
|
|
false;
|
|
};
|
|
},
|
|
|
|
"CHILD": function( type, what, argument, first, last ) {
|
|
var simple = type.slice( 0, 3 ) !== "nth",
|
|
forward = type.slice( -4 ) !== "last",
|
|
ofType = what === "of-type";
|
|
|
|
return first === 1 && last === 0 ?
|
|
|
|
// Shortcut for :nth-*(n)
|
|
function( elem ) {
|
|
return !!elem.parentNode;
|
|
} :
|
|
|
|
function( elem, context, xml ) {
|
|
var cache, outerCache, node, diff, nodeIndex, start,
|
|
dir = simple !== forward ? "nextSibling" : "previousSibling",
|
|
parent = elem.parentNode,
|
|
name = ofType && elem.nodeName.toLowerCase(),
|
|
useCache = !xml && !ofType;
|
|
|
|
if ( parent ) {
|
|
|
|
// :(first|last|only)-(child|of-type)
|
|
if ( simple ) {
|
|
while ( dir ) {
|
|
node = elem;
|
|
while ( (node = node[ dir ]) ) {
|
|
if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
|
|
return false;
|
|
}
|
|
}
|
|
// Reverse direction for :only-* (if we haven't yet done so)
|
|
start = dir = type === "only" && !start && "nextSibling";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
start = [ forward ? parent.firstChild : parent.lastChild ];
|
|
|
|
// non-xml :nth-child(...) stores cache data on `parent`
|
|
if ( forward && useCache ) {
|
|
// Seek `elem` from a previously-cached index
|
|
outerCache = parent[ expando ] || (parent[ expando ] = {});
|
|
cache = outerCache[ type ] || [];
|
|
nodeIndex = cache[0] === dirruns && cache[1];
|
|
diff = cache[0] === dirruns && cache[2];
|
|
node = nodeIndex && parent.childNodes[ nodeIndex ];
|
|
|
|
while ( (node = ++nodeIndex && node && node[ dir ] ||
|
|
|
|
// Fallback to seeking `elem` from the start
|
|
(diff = nodeIndex = 0) || start.pop()) ) {
|
|
|
|
// When found, cache indexes on `parent` and break
|
|
if ( node.nodeType === 1 && ++diff && node === elem ) {
|
|
outerCache[ type ] = [ dirruns, nodeIndex, diff ];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Use previously-cached element index if available
|
|
} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
|
|
diff = cache[1];
|
|
|
|
// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
|
|
} else {
|
|
// Use the same loop as above to seek `elem` from the start
|
|
while ( (node = ++nodeIndex && node && node[ dir ] ||
|
|
(diff = nodeIndex = 0) || start.pop()) ) {
|
|
|
|
if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
|
|
// Cache the index of each encountered element
|
|
if ( useCache ) {
|
|
(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
|
|
}
|
|
|
|
if ( node === elem ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Incorporate the offset, then check against cycle size
|
|
diff -= last;
|
|
return diff === first || ( diff % first === 0 && diff / first >= 0 );
|
|
}
|
|
};
|
|
},
|
|
|
|
"PSEUDO": function( pseudo, argument ) {
|
|
// pseudo-class names are case-insensitive
|
|
// http://www.w3.org/TR/selectors/#pseudo-classes
|
|
// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
|
|
// Remember that setFilters inherits from pseudos
|
|
var args,
|
|
fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
|
|
Sizzle.error( "unsupported pseudo: " + pseudo );
|
|
|
|
// The user may use createPseudo to indicate that
|
|
// arguments are needed to create the filter function
|
|
// just as Sizzle does
|
|
if ( fn[ expando ] ) {
|
|
return fn( argument );
|
|
}
|
|
|
|
// But maintain support for old signatures
|
|
if ( fn.length > 1 ) {
|
|
args = [ pseudo, pseudo, "", argument ];
|
|
return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
|
|
markFunction(function( seed, matches ) {
|
|
var idx,
|
|
matched = fn( seed, argument ),
|
|
i = matched.length;
|
|
while ( i-- ) {
|
|
idx = indexOf.call( seed, matched[i] );
|
|
seed[ idx ] = !( matches[ idx ] = matched[i] );
|
|
}
|
|
}) :
|
|
function( elem ) {
|
|
return fn( elem, 0, args );
|
|
};
|
|
}
|
|
|
|
return fn;
|
|
}
|
|
},
|
|
|
|
pseudos: {
|
|
// Potentially complex pseudos
|
|
"not": markFunction(function( selector ) {
|
|
// Trim the selector passed to compile
|
|
// to avoid treating leading and trailing
|
|
// spaces as combinators
|
|
var input = [],
|
|
results = [],
|
|
matcher = compile( selector.replace( rtrim, "$1" ) );
|
|
|
|
return matcher[ expando ] ?
|
|
markFunction(function( seed, matches, context, xml ) {
|
|
var elem,
|
|
unmatched = matcher( seed, null, xml, [] ),
|
|
i = seed.length;
|
|
|
|
// Match elements unmatched by `matcher`
|
|
while ( i-- ) {
|
|
if ( (elem = unmatched[i]) ) {
|
|
seed[i] = !(matches[i] = elem);
|
|
}
|
|
}
|
|
}) :
|
|
function( elem, context, xml ) {
|
|
input[0] = elem;
|
|
matcher( input, null, xml, results );
|
|
return !results.pop();
|
|
};
|
|
}),
|
|
|
|
"has": markFunction(function( selector ) {
|
|
return function( elem ) {
|
|
return Sizzle( selector, elem ).length > 0;
|
|
};
|
|
}),
|
|
|
|
"contains": markFunction(function( text ) {
|
|
return function( elem ) {
|
|
return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
|
|
};
|
|
}),
|
|
|
|
// "Whether an element is represented by a :lang() selector
|
|
// is based solely on the element's language value
|
|
// being equal to the identifier C,
|
|
// or beginning with the identifier C immediately followed by "-".
|
|
// The matching of C against the element's language value is performed case-insensitively.
|
|
// The identifier C does not have to be a valid language name."
|
|
// http://www.w3.org/TR/selectors/#lang-pseudo
|
|
"lang": markFunction( function( lang ) {
|
|
// lang value must be a valid identifier
|
|
if ( !ridentifier.test(lang || "") ) {
|
|
Sizzle.error( "unsupported lang: " + lang );
|
|
}
|
|
lang = lang.replace( runescape, funescape ).toLowerCase();
|
|
return function( elem ) {
|
|
var elemLang;
|
|
do {
|
|
if ( (elemLang = documentIsHTML ?
|
|
elem.lang :
|
|
elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
|
|
|
|
elemLang = elemLang.toLowerCase();
|
|
return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
|
|
}
|
|
} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
|
|
return false;
|
|
};
|
|
}),
|
|
|
|
// Miscellaneous
|
|
"target": function( elem ) {
|
|
var hash = window.location && window.location.hash;
|
|
return hash && hash.slice( 1 ) === elem.id;
|
|
},
|
|
|
|
"root": function( elem ) {
|
|
return elem === docElem;
|
|
},
|
|
|
|
"focus": function( elem ) {
|
|
return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
|
|
},
|
|
|
|
// Boolean properties
|
|
"enabled": function( elem ) {
|
|
return elem.disabled === false;
|
|
},
|
|
|
|
"disabled": function( elem ) {
|
|
return elem.disabled === true;
|
|
},
|
|
|
|
"checked": function( elem ) {
|
|
// In CSS3, :checked should return both checked and selected elements
|
|
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
|
|
var nodeName = elem.nodeName.toLowerCase();
|
|
return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
|
|
},
|
|
|
|
"selected": function( elem ) {
|
|
// Accessing this property makes selected-by-default
|
|
// options in Safari work properly
|
|
if ( elem.parentNode ) {
|
|
elem.parentNode.selectedIndex;
|
|
}
|
|
|
|
return elem.selected === true;
|
|
},
|
|
|
|
// Contents
|
|
"empty": function( elem ) {
|
|
// http://www.w3.org/TR/selectors/#empty-pseudo
|
|
// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
|
|
// not comment, processing instructions, or others
|
|
// Thanks to Diego Perini for the nodeName shortcut
|
|
// Greater than "@" means alpha characters (specifically not starting with "#" or "?")
|
|
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
|
|
if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
"parent": function( elem ) {
|
|
return !Expr.pseudos["empty"]( elem );
|
|
},
|
|
|
|
// Element/input types
|
|
"header": function( elem ) {
|
|
return rheader.test( elem.nodeName );
|
|
},
|
|
|
|
"input": function( elem ) {
|
|
return rinputs.test( elem.nodeName );
|
|
},
|
|
|
|
"button": function( elem ) {
|
|
var name = elem.nodeName.toLowerCase();
|
|
return name === "input" && elem.type === "button" || name === "button";
|
|
},
|
|
|
|
"text": function( elem ) {
|
|
var attr;
|
|
// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
|
|
// use getAttribute instead to test this case
|
|
return elem.nodeName.toLowerCase() === "input" &&
|
|
elem.type === "text" &&
|
|
( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type );
|
|
},
|
|
|
|
// Position-in-collection
|
|
"first": createPositionalPseudo(function() {
|
|
return [ 0 ];
|
|
}),
|
|
|
|
"last": createPositionalPseudo(function( matchIndexes, length ) {
|
|
return [ length - 1 ];
|
|
}),
|
|
|
|
"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
|
|
return [ argument < 0 ? argument + length : argument ];
|
|
}),
|
|
|
|
"even": createPositionalPseudo(function( matchIndexes, length ) {
|
|
var i = 0;
|
|
for ( ; i < length; i += 2 ) {
|
|
matchIndexes.push( i );
|
|
}
|
|
return matchIndexes;
|
|
}),
|
|
|
|
"odd": createPositionalPseudo(function( matchIndexes, length ) {
|
|
var i = 1;
|
|
for ( ; i < length; i += 2 ) {
|
|
matchIndexes.push( i );
|
|
}
|
|
return matchIndexes;
|
|
}),
|
|
|
|
"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
|
|
var i = argument < 0 ? argument + length : argument;
|
|
for ( ; --i >= 0; ) {
|
|
matchIndexes.push( i );
|
|
}
|
|
return matchIndexes;
|
|
}),
|
|
|
|
"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
|
|
var i = argument < 0 ? argument + length : argument;
|
|
for ( ; ++i < length; ) {
|
|
matchIndexes.push( i );
|
|
}
|
|
return matchIndexes;
|
|
})
|
|
}
|
|
};
|
|
|
|
Expr.pseudos["nth"] = Expr.pseudos["eq"];
|
|
|
|
// Add button/input type pseudos
|
|
for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
|
|
Expr.pseudos[ i ] = createInputPseudo( i );
|
|
}
|
|
for ( i in { submit: true, reset: true } ) {
|
|
Expr.pseudos[ i ] = createButtonPseudo( i );
|
|
}
|
|
|
|
// Easy API for creating new setFilters
|
|
function setFilters() {}
|
|
setFilters.prototype = Expr.filters = Expr.pseudos;
|
|
Expr.setFilters = new setFilters();
|
|
|
|
function tokenize( selector, parseOnly ) {
|
|
var matched, match, tokens, type,
|
|
soFar, groups, preFilters,
|
|
cached = tokenCache[ selector + " " ];
|
|
|
|
if ( cached ) {
|
|
return parseOnly ? 0 : cached.slice( 0 );
|
|
}
|
|
|
|
soFar = selector;
|
|
groups = [];
|
|
preFilters = Expr.preFilter;
|
|
|
|
while ( soFar ) {
|
|
|
|
// Comma and first run
|
|
if ( !matched || (match = rcomma.exec( soFar )) ) {
|
|
if ( match ) {
|
|
// Don't consume trailing commas as valid
|
|
soFar = soFar.slice( match[0].length ) || soFar;
|
|
}
|
|
groups.push( tokens = [] );
|
|
}
|
|
|
|
matched = false;
|
|
|
|
// Combinators
|
|
if ( (match = rcombinators.exec( soFar )) ) {
|
|
matched = match.shift();
|
|
tokens.push({
|
|
value: matched,
|
|
// Cast descendant combinators to space
|
|
type: match[0].replace( rtrim, " " )
|
|
});
|
|
soFar = soFar.slice( matched.length );
|
|
}
|
|
|
|
// Filters
|
|
for ( type in Expr.filter ) {
|
|
if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
|
|
(match = preFilters[ type ]( match ))) ) {
|
|
matched = match.shift();
|
|
tokens.push({
|
|
value: matched,
|
|
type: type,
|
|
matches: match
|
|
});
|
|
soFar = soFar.slice( matched.length );
|
|
}
|
|
}
|
|
|
|
if ( !matched ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Return the length of the invalid excess
|
|
// if we're just parsing
|
|
// Otherwise, throw an error or return tokens
|
|
return parseOnly ?
|
|
soFar.length :
|
|
soFar ?
|
|
Sizzle.error( selector ) :
|
|
// Cache the tokens
|
|
tokenCache( selector, groups ).slice( 0 );
|
|
}
|
|
|
|
function toSelector( tokens ) {
|
|
var i = 0,
|
|
len = tokens.length,
|
|
selector = "";
|
|
for ( ; i < len; i++ ) {
|
|
selector += tokens[i].value;
|
|
}
|
|
return selector;
|
|
}
|
|
|
|
function addCombinator( matcher, combinator, base ) {
|
|
var dir = combinator.dir,
|
|
checkNonElements = base && dir === "parentNode",
|
|
doneName = done++;
|
|
|
|
return combinator.first ?
|
|
// Check against closest ancestor/preceding element
|
|
function( elem, context, xml ) {
|
|
while ( (elem = elem[ dir ]) ) {
|
|
if ( elem.nodeType === 1 || checkNonElements ) {
|
|
return matcher( elem, context, xml );
|
|
}
|
|
}
|
|
} :
|
|
|
|
// Check against all ancestor/preceding elements
|
|
function( elem, context, xml ) {
|
|
var data, cache, outerCache,
|
|
dirkey = dirruns + " " + doneName;
|
|
|
|
// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
|
|
if ( xml ) {
|
|
while ( (elem = elem[ dir ]) ) {
|
|
if ( elem.nodeType === 1 || checkNonElements ) {
|
|
if ( matcher( elem, context, xml ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
while ( (elem = elem[ dir ]) ) {
|
|
if ( elem.nodeType === 1 || checkNonElements ) {
|
|
outerCache = elem[ expando ] || (elem[ expando ] = {});
|
|
if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {
|
|
if ( (data = cache[1]) === true || data === cachedruns ) {
|
|
return data === true;
|
|
}
|
|
} else {
|
|
cache = outerCache[ dir ] = [ dirkey ];
|
|
cache[1] = matcher( elem, context, xml ) || cachedruns;
|
|
if ( cache[1] === true ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function elementMatcher( matchers ) {
|
|
return matchers.length > 1 ?
|
|
function( elem, context, xml ) {
|
|
var i = matchers.length;
|
|
while ( i-- ) {
|
|
if ( !matchers[i]( elem, context, xml ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} :
|
|
matchers[0];
|
|
}
|
|
|
|
function condense( unmatched, map, filter, context, xml ) {
|
|
var elem,
|
|
newUnmatched = [],
|
|
i = 0,
|
|
len = unmatched.length,
|
|
mapped = map != null;
|
|
|
|
for ( ; i < len; i++ ) {
|
|
if ( (elem = unmatched[i]) ) {
|
|
if ( !filter || filter( elem, context, xml ) ) {
|
|
newUnmatched.push( elem );
|
|
if ( mapped ) {
|
|
map.push( i );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return newUnmatched;
|
|
}
|
|
|
|
function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
|
|
if ( postFilter && !postFilter[ expando ] ) {
|
|
postFilter = setMatcher( postFilter );
|
|
}
|
|
if ( postFinder && !postFinder[ expando ] ) {
|
|
postFinder = setMatcher( postFinder, postSelector );
|
|
}
|
|
return markFunction(function( seed, results, context, xml ) {
|
|
var temp, i, elem,
|
|
preMap = [],
|
|
postMap = [],
|
|
preexisting = results.length,
|
|
|
|
// Get initial elements from seed or context
|
|
elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
|
|
|
|
// Prefilter to get matcher input, preserving a map for seed-results synchronization
|
|
matcherIn = preFilter && ( seed || !selector ) ?
|
|
condense( elems, preMap, preFilter, context, xml ) :
|
|
elems,
|
|
|
|
matcherOut = matcher ?
|
|
// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
|
|
postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
|
|
|
|
// ...intermediate processing is necessary
|
|
[] :
|
|
|
|
// ...otherwise use results directly
|
|
results :
|
|
matcherIn;
|
|
|
|
// Find primary matches
|
|
if ( matcher ) {
|
|
matcher( matcherIn, matcherOut, context, xml );
|
|
}
|
|
|
|
// Apply postFilter
|
|
if ( postFilter ) {
|
|
temp = condense( matcherOut, postMap );
|
|
postFilter( temp, [], context, xml );
|
|
|
|
// Un-match failing elements by moving them back to matcherIn
|
|
i = temp.length;
|
|
while ( i-- ) {
|
|
if ( (elem = temp[i]) ) {
|
|
matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( seed ) {
|
|
if ( postFinder || preFilter ) {
|
|
if ( postFinder ) {
|
|
// Get the final matcherOut by condensing this intermediate into postFinder contexts
|
|
temp = [];
|
|
i = matcherOut.length;
|
|
while ( i-- ) {
|
|
if ( (elem = matcherOut[i]) ) {
|
|
// Restore matcherIn since elem is not yet a final match
|
|
temp.push( (matcherIn[i] = elem) );
|
|
}
|
|
}
|
|
postFinder( null, (matcherOut = []), temp, xml );
|
|
}
|
|
|
|
// Move matched elements from seed to results to keep them synchronized
|
|
i = matcherOut.length;
|
|
while ( i-- ) {
|
|
if ( (elem = matcherOut[i]) &&
|
|
(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
|
|
|
|
seed[temp] = !(results[temp] = elem);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add elements to results, through postFinder if defined
|
|
} else {
|
|
matcherOut = condense(
|
|
matcherOut === results ?
|
|
matcherOut.splice( preexisting, matcherOut.length ) :
|
|
matcherOut
|
|
);
|
|
if ( postFinder ) {
|
|
postFinder( null, results, matcherOut, xml );
|
|
} else {
|
|
push.apply( results, matcherOut );
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function matcherFromTokens( tokens ) {
|
|
var checkContext, matcher, j,
|
|
len = tokens.length,
|
|
leadingRelative = Expr.relative[ tokens[0].type ],
|
|
implicitRelative = leadingRelative || Expr.relative[" "],
|
|
i = leadingRelative ? 1 : 0,
|
|
|
|
// The foundational matcher ensures that elements are reachable from top-level context(s)
|
|
matchContext = addCombinator( function( elem ) {
|
|
return elem === checkContext;
|
|
}, implicitRelative, true ),
|
|
matchAnyContext = addCombinator( function( elem ) {
|
|
return indexOf.call( checkContext, elem ) > -1;
|
|
}, implicitRelative, true ),
|
|
matchers = [ function( elem, context, xml ) {
|
|
return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
|
|
(checkContext = context).nodeType ?
|
|
matchContext( elem, context, xml ) :
|
|
matchAnyContext( elem, context, xml ) );
|
|
} ];
|
|
|
|
for ( ; i < len; i++ ) {
|
|
if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
|
|
matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
|
|
} else {
|
|
matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
|
|
|
|
// Return special upon seeing a positional matcher
|
|
if ( matcher[ expando ] ) {
|
|
// Find the next relative operator (if any) for proper handling
|
|
j = ++i;
|
|
for ( ; j < len; j++ ) {
|
|
if ( Expr.relative[ tokens[j].type ] ) {
|
|
break;
|
|
}
|
|
}
|
|
return setMatcher(
|
|
i > 1 && elementMatcher( matchers ),
|
|
i > 1 && toSelector(
|
|
// If the preceding token was a descendant combinator, insert an implicit any-element `*`
|
|
tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
|
|
).replace( rtrim, "$1" ),
|
|
matcher,
|
|
i < j && matcherFromTokens( tokens.slice( i, j ) ),
|
|
j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
|
|
j < len && toSelector( tokens )
|
|
);
|
|
}
|
|
matchers.push( matcher );
|
|
}
|
|
}
|
|
|
|
return elementMatcher( matchers );
|
|
}
|
|
|
|
function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
|
|
// A counter to specify which element is currently being matched
|
|
var matcherCachedRuns = 0,
|
|
bySet = setMatchers.length > 0,
|
|
byElement = elementMatchers.length > 0,
|
|
superMatcher = function( seed, context, xml, results, expandContext ) {
|
|
var elem, j, matcher,
|
|
setMatched = [],
|
|
matchedCount = 0,
|
|
i = "0",
|
|
unmatched = seed && [],
|
|
outermost = expandContext != null,
|
|
contextBackup = outermostContext,
|
|
// We must always have either seed elements or context
|
|
elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
|
|
// Use integer dirruns iff this is the outermost matcher
|
|
dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1);
|
|
|
|
if ( outermost ) {
|
|
outermostContext = context !== document && context;
|
|
cachedruns = matcherCachedRuns;
|
|
}
|
|
|
|
// Add elements passing elementMatchers directly to results
|
|
// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
|
|
for ( ; (elem = elems[i]) != null; i++ ) {
|
|
if ( byElement && elem ) {
|
|
j = 0;
|
|
while ( (matcher = elementMatchers[j++]) ) {
|
|
if ( matcher( elem, context, xml ) ) {
|
|
results.push( elem );
|
|
break;
|
|
}
|
|
}
|
|
if ( outermost ) {
|
|
dirruns = dirrunsUnique;
|
|
cachedruns = ++matcherCachedRuns;
|
|
}
|
|
}
|
|
|
|
// Track unmatched elements for set filters
|
|
if ( bySet ) {
|
|
// They will have gone through all possible matchers
|
|
if ( (elem = !matcher && elem) ) {
|
|
matchedCount--;
|
|
}
|
|
|
|
// Lengthen the array for every element, matched or not
|
|
if ( seed ) {
|
|
unmatched.push( elem );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply set filters to unmatched elements
|
|
matchedCount += i;
|
|
if ( bySet && i !== matchedCount ) {
|
|
j = 0;
|
|
while ( (matcher = setMatchers[j++]) ) {
|
|
matcher( unmatched, setMatched, context, xml );
|
|
}
|
|
|
|
if ( seed ) {
|
|
// Reintegrate element matches to eliminate the need for sorting
|
|
if ( matchedCount > 0 ) {
|
|
while ( i-- ) {
|
|
if ( !(unmatched[i] || setMatched[i]) ) {
|
|
setMatched[i] = pop.call( results );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Discard index placeholder values to get only actual matches
|
|
setMatched = condense( setMatched );
|
|
}
|
|
|
|
// Add matches to results
|
|
push.apply( results, setMatched );
|
|
|
|
// Seedless set matches succeeding multiple successful matchers stipulate sorting
|
|
if ( outermost && !seed && setMatched.length > 0 &&
|
|
( matchedCount + setMatchers.length ) > 1 ) {
|
|
|
|
Sizzle.uniqueSort( results );
|
|
}
|
|
}
|
|
|
|
// Override manipulation of globals by nested matchers
|
|
if ( outermost ) {
|
|
dirruns = dirrunsUnique;
|
|
outermostContext = contextBackup;
|
|
}
|
|
|
|
return unmatched;
|
|
};
|
|
|
|
return bySet ?
|
|
markFunction( superMatcher ) :
|
|
superMatcher;
|
|
}
|
|
|
|
compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
|
|
var i,
|
|
setMatchers = [],
|
|
elementMatchers = [],
|
|
cached = compilerCache[ selector + " " ];
|
|
|
|
if ( !cached ) {
|
|
// Generate a function of recursive functions that can be used to check each element
|
|
if ( !group ) {
|
|
group = tokenize( selector );
|
|
}
|
|
i = group.length;
|
|
while ( i-- ) {
|
|
cached = matcherFromTokens( group[i] );
|
|
if ( cached[ expando ] ) {
|
|
setMatchers.push( cached );
|
|
} else {
|
|
elementMatchers.push( cached );
|
|
}
|
|
}
|
|
|
|
// Cache the compiled function
|
|
cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
|
|
}
|
|
return cached;
|
|
};
|
|
|
|
function multipleContexts( selector, contexts, results ) {
|
|
var i = 0,
|
|
len = contexts.length;
|
|
for ( ; i < len; i++ ) {
|
|
Sizzle( selector, contexts[i], results );
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function select( selector, context, results, seed ) {
|
|
var i, tokens, token, type, find,
|
|
match = tokenize( selector );
|
|
|
|
if ( !seed ) {
|
|
// Try to minimize operations if there is only one group
|
|
if ( match.length === 1 ) {
|
|
|
|
// Take a shortcut and set the context if the root selector is an ID
|
|
tokens = match[0] = match[0].slice( 0 );
|
|
if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
|
|
support.getById && context.nodeType === 9 && documentIsHTML &&
|
|
Expr.relative[ tokens[1].type ] ) {
|
|
|
|
context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
|
|
if ( !context ) {
|
|
return results;
|
|
}
|
|
selector = selector.slice( tokens.shift().value.length );
|
|
}
|
|
|
|
// Fetch a seed set for right-to-left matching
|
|
i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
|
|
while ( i-- ) {
|
|
token = tokens[i];
|
|
|
|
// Abort if we hit a combinator
|
|
if ( Expr.relative[ (type = token.type) ] ) {
|
|
break;
|
|
}
|
|
if ( (find = Expr.find[ type ]) ) {
|
|
// Search, expanding context for leading sibling combinators
|
|
if ( (seed = find(
|
|
token.matches[0].replace( runescape, funescape ),
|
|
rsibling.test( tokens[0].type ) && context.parentNode || context
|
|
)) ) {
|
|
|
|
// If seed is empty or no tokens remain, we can return early
|
|
tokens.splice( i, 1 );
|
|
selector = seed.length && toSelector( tokens );
|
|
if ( !selector ) {
|
|
push.apply( results, seed );
|
|
return results;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compile and execute a filtering function
|
|
// Provide `match` to avoid retokenization if we modified the selector above
|
|
compile( selector, match )(
|
|
seed,
|
|
context,
|
|
!documentIsHTML,
|
|
results,
|
|
rsibling.test( selector )
|
|
);
|
|
return results;
|
|
}
|
|
|
|
// One-time assignments
|
|
|
|
// Sort stability
|
|
support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
|
|
|
|
// Support: Chrome<14
|
|
// Always assume duplicates if they aren't passed to the comparison function
|
|
support.detectDuplicates = hasDuplicate;
|
|
|
|
// Initialize against the default document
|
|
setDocument();
|
|
|
|
// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
|
|
// Detached nodes confoundingly follow *each other*
|
|
support.sortDetached = assert(function( div1 ) {
|
|
// Should return 1, but returns 4 (following)
|
|
return div1.compareDocumentPosition( document.createElement("div") ) & 1;
|
|
});
|
|
|
|
// Support: IE<8
|
|
// Prevent attribute/property "interpolation"
|
|
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
|
|
if ( !assert(function( div ) {
|
|
div.innerHTML = "<a href='#'></a>";
|
|
return div.firstChild.getAttribute("href") === "#" ;
|
|
}) ) {
|
|
addHandle( "type|href|height|width", function( elem, name, isXML ) {
|
|
if ( !isXML ) {
|
|
return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
|
|
}
|
|
});
|
|
}
|
|
|
|
// Support: IE<9
|
|
// Use defaultValue in place of getAttribute("value")
|
|
if ( !support.attributes || !assert(function( div ) {
|
|
div.innerHTML = "<input/>";
|
|
div.firstChild.setAttribute( "value", "" );
|
|
return div.firstChild.getAttribute( "value" ) === "";
|
|
}) ) {
|
|
addHandle( "value", function( elem, name, isXML ) {
|
|
if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
|
|
return elem.defaultValue;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Support: IE<9
|
|
// Use getAttributeNode to fetch booleans when getAttribute lies
|
|
if ( !assert(function( div ) {
|
|
return div.getAttribute("disabled") == null;
|
|
}) ) {
|
|
addHandle( booleans, function( elem, name, isXML ) {
|
|
var val;
|
|
if ( !isXML ) {
|
|
return (val = elem.getAttributeNode( name )) && val.specified ?
|
|
val.value :
|
|
elem[ name ] === true ? name.toLowerCase() : null;
|
|
}
|
|
});
|
|
}
|
|
|
|
jQuery.find = Sizzle;
|
|
jQuery.expr = Sizzle.selectors;
|
|
jQuery.expr[":"] = jQuery.expr.pseudos;
|
|
jQuery.unique = Sizzle.uniqueSort;
|
|
jQuery.text = Sizzle.getText;
|
|
jQuery.isXMLDoc = Sizzle.isXML;
|
|
jQuery.contains = Sizzle.contains;
|
|
|
|
|
|
})( window );
|
|
// String to Object options format cache
|
|
var optionsCache = {};
|
|
|
|
// Convert String-formatted options into Object-formatted ones and store in cache
|
|
function createOptions( options ) {
|
|
var object = optionsCache[ options ] = {};
|
|
jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) {
|
|
object[ flag ] = true;
|
|
});
|
|
return object;
|
|
}
|
|
|
|
/*
|
|
* Create a callback list using the following parameters:
|
|
*
|
|
* options: an optional list of space-separated options that will change how
|
|
* the callback list behaves or a more traditional option object
|
|
*
|
|
* By default a callback list will act like an event callback list and can be
|
|
* "fired" multiple times.
|
|
*
|
|
* Possible options:
|
|
*
|
|
* once: will ensure the callback list can only be fired once (like a Deferred)
|
|
*
|
|
* memory: will keep track of previous values and will call any callback added
|
|
* after the list has been fired right away with the latest "memorized"
|
|
* values (like a Deferred)
|
|
*
|
|
* unique: will ensure a callback can only be added once (no duplicate in the list)
|
|
*
|
|
* stopOnFalse: interrupt callings when a callback returns false
|
|
*
|
|
*/
|
|
jQuery.Callbacks = function( options ) {
|
|
|
|
// Convert options from String-formatted to Object-formatted if needed
|
|
// (we check in cache first)
|
|
options = typeof options === "string" ?
|
|
( optionsCache[ options ] || createOptions( options ) ) :
|
|
jQuery.extend( {}, options );
|
|
|
|
var // Flag to know if list is currently firing
|
|
firing,
|
|
// Last fire value (for non-forgettable lists)
|
|
memory,
|
|
// Flag to know if list was already fired
|
|
fired,
|
|
// End of the loop when firing
|
|
firingLength,
|
|
// Index of currently firing callback (modified by remove if needed)
|
|
firingIndex,
|
|
// First callback to fire (used internally by add and fireWith)
|
|
firingStart,
|
|
// Actual callback list
|
|
list = [],
|
|
// Stack of fire calls for repeatable lists
|
|
stack = !options.once && [],
|
|
// Fire callbacks
|
|
fire = function( data ) {
|
|
memory = options.memory && data;
|
|
fired = true;
|
|
firingIndex = firingStart || 0;
|
|
firingStart = 0;
|
|
firingLength = list.length;
|
|
firing = true;
|
|
for ( ; list && firingIndex < firingLength; firingIndex++ ) {
|
|
if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
|
|
memory = false; // To prevent further calls using add
|
|
break;
|
|
}
|
|
}
|
|
firing = false;
|
|
if ( list ) {
|
|
if ( stack ) {
|
|
if ( stack.length ) {
|
|
fire( stack.shift() );
|
|
}
|
|
} else if ( memory ) {
|
|
list = [];
|
|
} else {
|
|
self.disable();
|
|
}
|
|
}
|
|
},
|
|
// Actual Callbacks object
|
|
self = {
|
|
// Add a callback or a collection of callbacks to the list
|
|
add: function() {
|
|
if ( list ) {
|
|
// First, we save the current length
|
|
var start = list.length;
|
|
(function add( args ) {
|
|
jQuery.each( args, function( _, arg ) {
|
|
var type = jQuery.type( arg );
|
|
if ( type === "function" ) {
|
|
if ( !options.unique || !self.has( arg ) ) {
|
|
list.push( arg );
|
|
}
|
|
} else if ( arg && arg.length && type !== "string" ) {
|
|
// Inspect recursively
|
|
add( arg );
|
|
}
|
|
});
|
|
})( arguments );
|
|
// Do we need to add the callbacks to the
|
|
// current firing batch?
|
|
if ( firing ) {
|
|
firingLength = list.length;
|
|
// With memory, if we're not firing then
|
|
// we should call right away
|
|
} else if ( memory ) {
|
|
firingStart = start;
|
|
fire( memory );
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
// Remove a callback from the list
|
|
remove: function() {
|
|
if ( list ) {
|
|
jQuery.each( arguments, function( _, arg ) {
|
|
var index;
|
|
while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
|
|
list.splice( index, 1 );
|
|
// Handle firing indexes
|
|
if ( firing ) {
|
|
if ( index <= firingLength ) {
|
|
firingLength--;
|
|
}
|
|
if ( index <= firingIndex ) {
|
|
firingIndex--;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return this;
|
|
},
|
|
// Check if a given callback is in the list.
|
|
// If no argument is given, return whether or not list has callbacks attached.
|
|
has: function( fn ) {
|
|
return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
|
|
},
|
|
// Remove all callbacks from the list
|
|
empty: function() {
|
|
list = [];
|
|
firingLength = 0;
|
|
return this;
|
|
},
|
|
// Have the list do nothing anymore
|
|
disable: function() {
|
|
list = stack = memory = undefined;
|
|
return this;
|
|
},
|
|
// Is it disabled?
|
|
disabled: function() {
|
|
return !list;
|
|
},
|
|
// Lock the list in its current state
|
|
lock: function() {
|
|
stack = undefined;
|
|
if ( !memory ) {
|
|
self.disable();
|
|
}
|
|
return this;
|
|
},
|
|
// Is it locked?
|
|
locked: function() {
|
|
return !stack;
|
|
},
|
|
// Call all callbacks with the given context and arguments
|
|
fireWith: function( context, args ) {
|
|
if ( list && ( !fired || stack ) ) {
|
|
args = args || [];
|
|
args = [ context, args.slice ? args.slice() : args ];
|
|
if ( firing ) {
|
|
stack.push( args );
|
|
} else {
|
|
fire( args );
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
// Call all the callbacks with the given arguments
|
|
fire: function() {
|
|
self.fireWith( this, arguments );
|
|
return this;
|
|
},
|
|
// To know if the callbacks have already been called at least once
|
|
fired: function() {
|
|
return !!fired;
|
|
}
|
|
};
|
|
|
|
return self;
|
|
};
|
|
jQuery.extend({
|
|
|
|
Deferred: function( func ) {
|
|
var tuples = [
|
|
// action, add listener, listener list, final state
|
|
[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
|
|
[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
|
|
[ "notify", "progress", jQuery.Callbacks("memory") ]
|
|
],
|
|
state = "pending",
|
|
promise = {
|
|
state: function() {
|
|
return state;
|
|
},
|
|
always: function() {
|
|
deferred.done( arguments ).fail( arguments );
|
|
return this;
|
|
},
|
|
then: function( /* fnDone, fnFail, fnProgress */ ) {
|
|
var fns = arguments;
|
|
return jQuery.Deferred(function( newDefer ) {
|
|
jQuery.each( tuples, function( i, tuple ) {
|
|
var action = tuple[ 0 ],
|
|
fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
|
|
// deferred[ done | fail | progress ] for forwarding actions to newDefer
|
|
deferred[ tuple[1] ](function() {
|
|
var returned = fn && fn.apply( this, arguments );
|
|
if ( returned && jQuery.isFunction( returned.promise ) ) {
|
|
returned.promise()
|
|
.done( newDefer.resolve )
|
|
.fail( newDefer.reject )
|
|
.progress( newDefer.notify );
|
|
} else {
|
|
newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
|
|
}
|
|
});
|
|
});
|
|
fns = null;
|
|
}).promise();
|
|
},
|
|
// Get a promise for this deferred
|
|
// If obj is provided, the promise aspect is added to the object
|
|
promise: function( obj ) {
|
|
return obj != null ? jQuery.extend( obj, promise ) : promise;
|
|
}
|
|
},
|
|
deferred = {};
|
|
|
|
// Keep pipe for back-compat
|
|
promise.pipe = promise.then;
|
|
|
|
// Add list-specific methods
|
|
jQuery.each( tuples, function( i, tuple ) {
|
|
var list = tuple[ 2 ],
|
|
stateString = tuple[ 3 ];
|
|
|
|
// promise[ done | fail | progress ] = list.add
|
|
promise[ tuple[1] ] = list.add;
|
|
|
|
// Handle state
|
|
if ( stateString ) {
|
|
list.add(function() {
|
|
// state = [ resolved | rejected ]
|
|
state = stateString;
|
|
|
|
// [ reject_list | resolve_list ].disable; progress_list.lock
|
|
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
|
|
}
|
|
|
|
// deferred[ resolve | reject | notify ]
|
|
deferred[ tuple[0] ] = function() {
|
|
deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
|
|
return this;
|
|
};
|
|
deferred[ tuple[0] + "With" ] = list.fireWith;
|
|
});
|
|
|
|
// Make the deferred a promise
|
|
promise.promise( deferred );
|
|
|
|
// Call given func if any
|
|
if ( func ) {
|
|
func.call( deferred, deferred );
|
|
}
|
|
|
|
// All done!
|
|
return deferred;
|
|
},
|
|
|
|
// Deferred helper
|
|
when: function( subordinate /* , ..., subordinateN */ ) {
|
|
var i = 0,
|
|
resolveValues = core_slice.call( arguments ),
|
|
length = resolveValues.length,
|
|
|
|
// the count of uncompleted subordinates
|
|
remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
|
|
|
|
// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
|
|
deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
|
|
|
|
// Update function for both resolve and progress values
|
|
updateFunc = function( i, contexts, values ) {
|
|
return function( value ) {
|
|
contexts[ i ] = this;
|
|
values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
|
|
if( values === progressValues ) {
|
|
deferred.notifyWith( contexts, values );
|
|
} else if ( !( --remaining ) ) {
|
|
deferred.resolveWith( contexts, values );
|
|
}
|
|
};
|
|
},
|
|
|
|
progressValues, progressContexts, resolveContexts;
|
|
|
|
// add listeners to Deferred subordinates; treat others as resolved
|
|
if ( length > 1 ) {
|
|
progressValues = new Array( length );
|
|
progressContexts = new Array( length );
|
|
resolveContexts = new Array( length );
|
|
for ( ; i < length; i++ ) {
|
|
if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
|
|
resolveValues[ i ].promise()
|
|
.done( updateFunc( i, resolveContexts, resolveValues ) )
|
|
.fail( deferred.reject )
|
|
.progress( updateFunc( i, progressContexts, progressValues ) );
|
|
} else {
|
|
--remaining;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we're not waiting on anything, resolve the master
|
|
if ( !remaining ) {
|
|
deferred.resolveWith( resolveContexts, resolveValues );
|
|
}
|
|
|
|
return deferred.promise();
|
|
}
|
|
});
|
|
jQuery.support = (function( support ) {
|
|
|
|
var all, a, input, select, fragment, opt, eventName, isSupported, i,
|
|
div = document.createElement("div");
|
|
|
|
// Setup
|
|
div.setAttribute( "className", "t" );
|
|
div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
|
|
|
|
// Finish early in limited (non-browser) environments
|
|
all = div.getElementsByTagName("*") || [];
|
|
a = div.getElementsByTagName("a")[ 0 ];
|
|
if ( !a || !a.style || !all.length ) {
|
|
return support;
|
|
}
|
|
|
|
// First batch of tests
|
|
select = document.createElement("select");
|
|
opt = select.appendChild( document.createElement("option") );
|
|
input = div.getElementsByTagName("input")[ 0 ];
|
|
|
|
a.style.cssText = "top:1px;float:left;opacity:.5";
|
|
|
|
// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
|
|
support.getSetAttribute = div.className !== "t";
|
|
|
|
// IE strips leading whitespace when .innerHTML is used
|
|
support.leadingWhitespace = div.firstChild.nodeType === 3;
|
|
|
|
// Make sure that tbody elements aren't automatically inserted
|
|
// IE will insert them into empty tables
|
|
support.tbody = !div.getElementsByTagName("tbody").length;
|
|
|
|
// Make sure that link elements get serialized correctly by innerHTML
|
|
// This requires a wrapper element in IE
|
|
support.htmlSerialize = !!div.getElementsByTagName("link").length;
|
|
|
|
// Get the style information from getAttribute
|
|
// (IE uses .cssText instead)
|
|
support.style = /top/.test( a.getAttribute("style") );
|
|
|
|
// Make sure that URLs aren't manipulated
|
|
// (IE normalizes it by default)
|
|
support.hrefNormalized = a.getAttribute("href") === "/a";
|
|
|
|
// Make sure that element opacity exists
|
|
// (IE uses filter instead)
|
|
// Use a regex to work around a WebKit issue. See #5145
|
|
support.opacity = /^0.5/.test( a.style.opacity );
|
|
|
|
// Verify style float existence
|
|
// (IE uses styleFloat instead of cssFloat)
|
|
support.cssFloat = !!a.style.cssFloat;
|
|
|
|
// Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
|
|
support.checkOn = !!input.value;
|
|
|
|
// Make sure that a selected-by-default option has a working selected property.
|
|
// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
|
|
support.optSelected = opt.selected;
|
|
|
|
// Tests for enctype support on a form (#6743)
|
|
support.enctype = !!document.createElement("form").enctype;
|
|
|
|
// Makes sure cloning an html5 element does not cause problems
|
|
// Where outerHTML is undefined, this still works
|
|
support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>";
|
|
|
|
// Will be defined later
|
|
support.inlineBlockNeedsLayout = false;
|
|
support.shrinkWrapBlocks = false;
|
|
support.pixelPosition = false;
|
|
support.deleteExpando = true;
|
|
support.noCloneEvent = true;
|
|
support.reliableMarginRight = true;
|
|
support.boxSizingReliable = true;
|
|
|
|
// Make sure checked status is properly cloned
|
|
input.checked = true;
|
|
support.noCloneChecked = input.cloneNode( true ).checked;
|
|
|
|
// Make sure that the options inside disabled selects aren't marked as disabled
|
|
// (WebKit marks them as disabled)
|
|
select.disabled = true;
|
|
support.optDisabled = !opt.disabled;
|
|
|
|
// Support: IE<9
|
|
try {
|
|
delete div.test;
|
|
} catch( e ) {
|
|
support.deleteExpando = false;
|
|
}
|
|
|
|
// Check if we can trust getAttribute("value")
|
|
input = document.createElement("input");
|
|
input.setAttribute( "value", "" );
|
|
support.input = input.getAttribute( "value" ) === "";
|
|
|
|
// Check if an input maintains its value after becoming a radio
|
|
input.value = "t";
|
|
input.setAttribute( "type", "radio" );
|
|
support.radioValue = input.value === "t";
|
|
|
|
// #11217 - WebKit loses check when the name is after the checked attribute
|
|
input.setAttribute( "checked", "t" );
|
|
input.setAttribute( "name", "t" );
|
|
|
|
fragment = document.createDocumentFragment();
|
|
fragment.appendChild( input );
|
|
|
|
// Check if a disconnected checkbox will retain its checked
|
|
// value of true after appended to the DOM (IE6/7)
|
|
support.appendChecked = input.checked;
|
|
|
|
// WebKit doesn't clone checked state correctly in fragments
|
|
support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
|
|
|
|
// Support: IE<9
|
|
// Opera does not clone events (and typeof div.attachEvent === undefined).
|
|
// IE9-10 clones events bound via attachEvent, but they don't trigger with .click()
|
|
if ( div.attachEvent ) {
|
|
div.attachEvent( "onclick", function() {
|
|
support.noCloneEvent = false;
|
|
});
|
|
|
|
div.cloneNode( true ).click();
|
|
}
|
|
|
|
// Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event)
|
|
// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)
|
|
for ( i in { submit: true, change: true, focusin: true }) {
|
|
div.setAttribute( eventName = "on" + i, "t" );
|
|
|
|
support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false;
|
|
}
|
|
|
|
div.style.backgroundClip = "content-box";
|
|
div.cloneNode( true ).style.backgroundClip = "";
|
|
support.clearCloneStyle = div.style.backgroundClip === "content-box";
|
|
|
|
// Support: IE<9
|
|
// Iteration over object's inherited properties before its own.
|
|
for ( i in jQuery( support ) ) {
|
|
break;
|
|
}
|
|
support.ownLast = i !== "0";
|
|
|
|
// Run tests that need a body at doc ready
|
|
jQuery(function() {
|
|
var container, marginDiv, tds,
|
|
divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",
|
|
body = document.getElementsByTagName("body")[0];
|
|
|
|
if ( !body ) {
|
|
// Return for frameset docs that don't have a body
|
|
return;
|
|
}
|
|
|
|
container = document.createElement("div");
|
|
container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";
|
|
|
|
body.appendChild( container ).appendChild( div );
|
|
|
|
// Support: IE8
|
|
// Check if table cells still have offsetWidth/Height when they are set
|
|
// to display:none and there are still other visible table cells in a
|
|
// table row; if so, offsetWidth/Height are not reliable for use when
|
|
// determining if an element has been hidden directly using
|
|
// display:none (it is still safe to use offsets if a parent element is
|
|
// hidden; don safety goggles and see bug #4512 for more information).
|
|
div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
|
|
tds = div.getElementsByTagName("td");
|
|
tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
|
|
isSupported = ( tds[ 0 ].offsetHeight === 0 );
|
|
|
|
tds[ 0 ].style.display = "";
|
|
tds[ 1 ].style.display = "none";
|
|
|
|
// Support: IE8
|
|
// Check if empty table cells still have offsetWidth/Height
|
|
support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
|
|
|
|
// Check box-sizing and margin behavior.
|
|
div.innerHTML = "";
|
|
div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
|
|
|
|
// Workaround failing boxSizing test due to offsetWidth returning wrong value
|
|
// with some non-1 values of body zoom, ticket #13543
|
|
jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() {
|
|
support.boxSizing = div.offsetWidth === 4;
|
|
});
|
|
|
|
// Use window.getComputedStyle because jsdom on node.js will break without it.
|
|
if ( window.getComputedStyle ) {
|
|
support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
|
|
support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
|
|
|
|
// Check if div with explicit width and no margin-right incorrectly
|
|
// gets computed margin-right based on width of container. (#3333)
|
|
// Fails in WebKit before Feb 2011 nightlies
|
|
// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
|
|
marginDiv = div.appendChild( document.createElement("div") );
|
|
marginDiv.style.cssText = div.style.cssText = divReset;
|
|
marginDiv.style.marginRight = marginDiv.style.width = "0";
|
|
div.style.width = "1px";
|
|
|
|
support.reliableMarginRight =
|
|
!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
|
|
}
|
|
|
|
if ( typeof div.style.zoom !== core_strundefined ) {
|
|
// Support: IE<8
|
|
// Check if natively block-level elements act like inline-block
|
|
// elements when setting their display to 'inline' and giving
|
|
// them layout
|
|
div.innerHTML = "";
|
|
div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
|
|
support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
|
|
|
|
// Support: IE6
|
|
// Check if elements with layout shrink-wrap their children
|
|
div.style.display = "block";
|
|
div.innerHTML = "<div></div>";
|
|
div.firstChild.style.width = "5px";
|
|
support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
|
|
|
|
if ( support.inlineBlockNeedsLayout ) {
|
|
// Prevent IE 6 from affecting layout for positioned elements #11048
|
|
// Prevent IE from shrinking the body in IE 7 mode #12869
|
|
// Support: IE<8
|
|
body.style.zoom = 1;
|
|
}
|
|
}
|
|
|
|
body.removeChild( container );
|
|
|
|
// Null elements to avoid leaks in IE
|
|
container = div = tds = marginDiv = null;
|
|
});
|
|
|
|
// Null elements to avoid leaks in IE
|
|
all = select = fragment = opt = a = input = null;
|
|
|
|
return support;
|
|
})({});
|
|
|
|
var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
|
|
rmultiDash = /([A-Z])/g;
|
|
|
|
function internalData( elem, name, data, pvt /* Internal Use Only */ ){
|
|
if ( !jQuery.acceptData( elem ) ) {
|
|
return;
|
|
}
|
|
|
|
var ret, thisCache,
|
|
internalKey = jQuery.expando,
|
|
|
|
// We have to handle DOM nodes and JS objects differently because IE6-7
|
|
// can't GC object references properly across the DOM-JS boundary
|
|
isNode = elem.nodeType,
|
|
|
|
// Only DOM nodes need the global jQuery cache; JS object data is
|
|
// attached directly to the object so GC can occur automatically
|
|
cache = isNode ? jQuery.cache : elem,
|
|
|
|
// Only defining an ID for JS objects if its cache already exists allows
|
|
// the code to shortcut on the same path as a DOM node with no cache
|
|
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
|
|
|
|
// Avoid doing any more work than we need to when trying to get data on an
|
|
// object that has no data at all
|
|
if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) {
|
|
return;
|
|
}
|
|
|
|
if ( !id ) {
|
|
// Only DOM nodes need a new unique ID for each element since their data
|
|
// ends up in the global cache
|
|
if ( isNode ) {
|
|
id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++;
|
|
} else {
|
|
id = internalKey;
|
|
}
|
|
}
|
|
|
|
if ( !cache[ id ] ) {
|
|
// Avoid exposing jQuery metadata on plain JS objects when the object
|
|
// is serialized using JSON.stringify
|
|
cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
|
|
}
|
|
|
|
// An object can be passed to jQuery.data instead of a key/value pair; this gets
|
|
// shallow copied over onto the existing cache
|
|
if ( typeof name === "object" || typeof name === "function" ) {
|
|
if ( pvt ) {
|
|
cache[ id ] = jQuery.extend( cache[ id ], name );
|
|
} else {
|
|
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
|
|
}
|
|
}
|
|
|
|
thisCache = cache[ id ];
|
|
|
|
// jQuery data() is stored in a separate object inside the object's internal data
|
|
// cache in order to avoid key collisions between internal data and user-defined
|
|
// data.
|
|
if ( !pvt ) {
|
|
if ( !thisCache.data ) {
|
|
thisCache.data = {};
|
|
}
|
|
|
|
thisCache = thisCache.data;
|
|
}
|
|
|
|
if ( data !== undefined ) {
|
|
thisCache[ jQuery.camelCase( name ) ] = data;
|
|
}
|
|
|
|
// Check for both converted-to-camel and non-converted data property names
|
|
// If a data property was specified
|
|
if ( typeof name === "string" ) {
|
|
|
|
// First Try to find as-is property data
|
|
ret = thisCache[ name ];
|
|
|
|
// Test for null|undefined property data
|
|
if ( ret == null ) {
|
|
|
|
// Try to find the camelCased property
|
|
ret = thisCache[ jQuery.camelCase( name ) ];
|
|
}
|
|
} else {
|
|
ret = thisCache;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function internalRemoveData( elem, name, pvt ) {
|
|
if ( !jQuery.acceptData( elem ) ) {
|
|
return;
|
|
}
|
|
|
|
var thisCache, i,
|
|
isNode = elem.nodeType,
|
|
|
|
// See jQuery.data for more information
|
|
cache = isNode ? jQuery.cache : elem,
|
|
id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
|
|
|
|
// If there is already no cache entry for this object, there is no
|
|
// purpose in continuing
|
|
if ( !cache[ id ] ) {
|
|
return;
|
|
}
|
|
|
|
if ( name ) {
|
|
|
|
thisCache = pvt ? cache[ id ] : cache[ id ].data;
|
|
|
|
if ( thisCache ) {
|
|
|
|
// Support array or space separated string names for data keys
|
|
if ( !jQuery.isArray( name ) ) {
|
|
|
|
// try the string as a key before any manipulation
|
|
if ( name in thisCache ) {
|
|
name = [ name ];
|
|
} else {
|
|
|
|
// split the camel cased version by spaces unless a key with the spaces exists
|
|
name = jQuery.camelCase( name );
|
|
if ( name in thisCache ) {
|
|
name = [ name ];
|
|
} else {
|
|
name = name.split(" ");
|
|
}
|
|
}
|
|
} else {
|
|
// If "name" is an array of keys...
|
|
// When data is initially created, via ("key", "val") signature,
|
|
// keys will be converted to camelCase.
|
|
// Since there is no way to tell _how_ a key was added, remove
|
|
// both plain key and camelCase key. #12786
|
|
// This will only penalize the array argument path.
|
|
name = name.concat( jQuery.map( name, jQuery.camelCase ) );
|
|
}
|
|
|
|
i = name.length;
|
|
while ( i-- ) {
|
|
delete thisCache[ name[i] ];
|
|
}
|
|
|
|
// If there is no data left in the cache, we want to continue
|
|
// and let the cache object itself get destroyed
|
|
if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// See jQuery.data for more information
|
|
if ( !pvt ) {
|
|
delete cache[ id ].data;
|
|
|
|
// Don't destroy the parent cache unless the internal data object
|
|
// had been the only thing left in it
|
|
if ( !isEmptyDataObject( cache[ id ] ) ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Destroy the cache
|
|
if ( isNode ) {
|
|
jQuery.cleanData( [ elem ], true );
|
|
|
|
// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
|
|
/* jshint eqeqeq: false */
|
|
} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
|
|
/* jshint eqeqeq: true */
|
|
delete cache[ id ];
|
|
|
|
// When all else fails, null
|
|
} else {
|
|
cache[ id ] = null;
|
|
}
|
|
}
|
|
|
|
jQuery.extend({
|
|
cache: {},
|
|
|
|
// The following elements throw uncatchable exceptions if you
|
|
// attempt to add expando properties to them.
|
|
noData: {
|
|
"applet": true,
|
|
"embed": true,
|
|
// Ban all objects except for Flash (which handle expandos)
|
|
"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
|
|
},
|
|
|
|
hasData: function( elem ) {
|
|
elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
|
|
return !!elem && !isEmptyDataObject( elem );
|
|
},
|
|
|
|
data: function( elem, name, data ) {
|
|
return internalData( elem, name, data );
|
|
},
|
|
|
|
removeData: function( elem, name ) {
|
|
return internalRemoveData( elem, name );
|
|
},
|
|
|
|
// For internal use only.
|
|
_data: function( elem, name, data ) {
|
|
return internalData( elem, name, data, true );
|
|
},
|
|
|
|
_removeData: function( elem, name ) {
|
|
return internalRemoveData( elem, name, true );
|
|
},
|
|
|
|
// A method for determining if a DOM node can handle the data expando
|
|
acceptData: function( elem ) {
|
|
// Do not set data on non-element because it will not be cleared (#8335).
|
|
if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) {
|
|
return false;
|
|
}
|
|
|
|
var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
|
|
|
|
// nodes accept data unless otherwise specified; rejection can be conditional
|
|
return !noData || noData !== true && elem.getAttribute("classid") === noData;
|
|
}
|
|
});
|
|
|
|
jQuery.fn.extend({
|
|
data: function( key, value ) {
|
|
var attrs, name,
|
|
data = null,
|
|
i = 0,
|
|
elem = this[0];
|
|
|
|
// Special expections of .data basically thwart jQuery.access,
|
|
// so implement the relevant behavior ourselves
|
|
|
|
// Gets all values
|
|
if ( key === undefined ) {
|
|
if ( this.length ) {
|
|
data = jQuery.data( elem );
|
|
|
|
if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
|
|
attrs = elem.attributes;
|
|
for ( ; i < attrs.length; i++ ) {
|
|
name = attrs[i].name;
|
|
|
|
if ( name.indexOf("data-") === 0 ) {
|
|
name = jQuery.camelCase( name.slice(5) );
|
|
|
|
dataAttr( elem, name, data[ name ] );
|
|
}
|
|
}
|
|
jQuery._data( elem, "parsedAttrs", true );
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Sets multiple values
|
|
if ( typeof key === "object" ) {
|
|
return this.each(function() {
|
|
jQuery.data( this, key );
|
|
});
|
|
}
|
|
|
|
return arguments.length > 1 ?
|
|
|
|
// Sets one value
|
|
this.each(function() {
|
|
jQuery.data( this, key, value );
|
|
}) :
|
|
|
|
// Gets one value
|
|
// Try to fetch any internally stored data first
|
|
elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
|
|
},
|
|
|
|
removeData: function( key ) {
|
|
return this.each(function() {
|
|
jQuery.removeData( this, key );
|
|
});
|
|
}
|
|
});
|
|
|
|
function dataAttr( elem, key, data ) {
|
|
// If nothing was found internally, try to fetch any
|
|
// data from the HTML5 data-* attribute
|
|
if ( data === undefined && elem.nodeType === 1 ) {
|
|
|
|
var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
|
|
|
|
data = elem.getAttribute( name );
|
|
|
|
if ( typeof data === "string" ) {
|
|
try {
|
|
data = data === "true" ? true :
|
|
data === "false" ? false :
|
|
data === "null" ? null :
|
|
// Only convert to a number if it doesn't change the string
|
|
+data + "" === data ? +data :
|
|
rbrace.test( data ) ? jQuery.parseJSON( data ) :
|
|
data;
|
|
} catch( e ) {}
|
|
|
|
// Make sure we set the data so it isn't changed later
|
|
jQuery.data( elem, key, data );
|
|
|
|
} else {
|
|
data = undefined;
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// checks a cache object for emptiness
|
|
function isEmptyDataObject( obj ) {
|
|
var name;
|
|
for ( name in obj ) {
|
|
|
|
// if the public data object is empty, the private is still empty
|
|
if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
|
|
continue;
|
|
}
|
|
if ( name !== "toJSON" ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
jQuery.extend({
|
|
queue: function( elem, type, data ) {
|
|
var queue;
|
|
|
|
if ( elem ) {
|
|
type = ( type || "fx" ) + "queue";
|
|
queue = jQuery._data( elem, type );
|
|
|
|
// Speed up dequeue by getting out quickly if this is just a lookup
|
|
if ( data ) {
|
|
if ( !queue || jQuery.isArray(data) ) {
|
|
queue = jQuery._data( elem, type, jQuery.makeArray(data) );
|
|
} else {
|
|
queue.push( data );
|
|
}
|
|
}
|
|
return queue || [];
|
|
}
|
|
},
|
|
|
|
dequeue: function( elem, type ) {
|
|
type = type || "fx";
|
|
|
|
var queue = jQuery.queue( elem, type ),
|
|
startLength = queue.length,
|
|
fn = queue.shift(),
|
|
hooks = jQuery._queueHooks( elem, type ),
|
|
next = function() {
|
|
jQuery.dequeue( elem, type );
|
|
};
|
|
|
|
// If the fx queue is dequeued, always remove the progress sentinel
|
|
if ( fn === "inprogress" ) {
|
|
fn = queue.shift();
|
|
startLength--;
|
|
}
|
|
|
|
if ( fn ) {
|
|
|
|
// Add a progress sentinel to prevent the fx queue from being
|
|
// automatically dequeued
|
|
if ( type === "fx" ) {
|
|
queue.unshift( "inprogress" );
|
|
}
|
|
|
|
// clear up the last queue stop function
|
|
delete hooks.stop;
|
|
fn.call( elem, next, hooks );
|
|
}
|
|
|
|
if ( !startLength && hooks ) {
|
|
hooks.empty.fire();
|
|
}
|
|
},
|
|
|
|
// not intended for public consumption - generates a queueHooks object, or returns the current one
|
|
_queueHooks: function( elem, type ) {
|
|
var key = type + "queueHooks";
|
|
return jQuery._data( elem, key ) || jQuery._data( elem, key, {
|
|
empty: jQuery.Callbacks("once memory").add(function() {
|
|
jQuery._removeData( elem, type + "queue" );
|
|
jQuery._removeData( elem, key );
|
|
})
|
|
});
|
|
}
|
|
});
|
|
|
|
jQuery.fn.extend({
|
|
queue: function( type, data ) {
|
|
var setter = 2;
|
|
|
|
if ( typeof type !== "string" ) {
|
|
data = type;
|
|
type = "fx";
|
|
setter--;
|
|
}
|
|
|
|
if ( arguments.length < setter ) {
|
|
return jQuery.queue( this[0], type );
|
|
}
|
|
|
|
return data === undefined ?
|
|
this :
|
|
this.each(function() {
|
|
var queue = jQuery.queue( this, type, data );
|
|
|
|
// ensure a hooks for this queue
|
|
jQuery._queueHooks( this, type );
|
|
|
|
if ( type === "fx" && queue[0] !== "inprogress" ) {
|
|
jQuery.dequeue( this, type );
|
|
}
|
|
});
|
|
},
|
|
dequeue: function( type ) {
|
|
return this.each(function() {
|
|
jQuery.dequeue( this, type );
|
|
});
|
|
},
|
|
// Based off of the plugin by Clint Helfers, with permission.
|
|
// http://blindsignals.com/index.php/2009/07/jquery-delay/
|
|
delay: function( time, type ) {
|
|
time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
|
|
type = type || "fx";
|
|
|
|
return this.queue( type, function( next, hooks ) {
|
|
var timeout = setTimeout( next, time );
|
|
hooks.stop = function() {
|
|
clearTimeout( timeout );
|
|
};
|
|
});
|
|
},
|
|
clearQueue: function( type ) {
|
|
return this.queue( type || "fx", [] );
|
|
},
|
|
// Get a promise resolved when queues of a certain type
|
|
// are emptied (fx is the type by default)
|
|
promise: function( type, obj ) {
|
|
var tmp,
|
|
count = 1,
|
|
defer = jQuery.Deferred(),
|
|
elements = this,
|
|
i = this.length,
|
|
resolve = function() {
|
|
if ( !( --count ) ) {
|
|
defer.resolveWith( elements, [ elements ] );
|
|
}
|
|
};
|
|
|
|
if ( typeof type !== "string" ) {
|
|
obj = type;
|
|
type = undefined;
|
|
}
|
|
type = type || "fx";
|
|
|
|
while( i-- ) {
|
|
tmp = jQuery._data( elements[ i ], type + "queueHooks" );
|
|
if ( tmp && tmp.empty ) {
|
|
count++;
|
|
tmp.empty.add( resolve );
|
|
}
|
|
}
|
|
resolve();
|
|
return defer.promise( obj );
|
|
}
|
|
});
|
|
var nodeHook, boolHook,
|
|
rclass = /[\t\r\n\f]/g,
|
|
rreturn = /\r/g,
|
|
rfocusable = /^(?:input|select|textarea|button|object)$/i,
|
|
rclickable = /^(?:a|area)$/i,
|
|
ruseDefault = /^(?:checked|selected)$/i,
|
|
getSetAttribute = jQuery.support.getSetAttribute,
|
|
getSetInput = jQuery.support.input;
|
|
|
|
jQuery.fn.extend({
|
|
attr: function( name, value ) {
|
|
return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
|
|
},
|
|
|
|
removeAttr: function( name ) {
|
|
return this.each(function() {
|
|
jQuery.removeAttr( this, name );
|
|
});
|
|
},
|
|
|
|
prop: function( name, value ) {
|
|
return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
|
|
},
|
|
|
|
removeProp: function( name ) {
|
|
name = jQuery.propFix[ name ] || name;
|
|
return this.each(function() {
|
|
// try/catch handles cases where IE balks (such as removing a property on window)
|
|
try {
|
|
this[ name ] = undefined;
|
|
delete this[ name ];
|
|
} catch( e ) {}
|
|
});
|
|
},
|
|
|
|
addClass: function( value ) {
|
|
var classes, elem, cur, clazz, j,
|
|
i = 0,
|
|
len = this.length,
|
|
proceed = typeof value === "string" && value;
|
|
|
|
if ( jQuery.isFunction( value ) ) {
|
|
return this.each(function( j ) {
|
|
jQuery( this ).addClass( value.call( this, j, this.className ) );
|
|
});
|
|
}
|
|
|
|
if ( proceed ) {
|
|
// The disjunction here is for better compressibility (see removeClass)
|
|
classes = ( value || "" ).match( core_rnotwhite ) || [];
|
|
|
|
for ( ; i < len; i++ ) {
|
|
elem = this[ i ];
|
|
cur = elem.nodeType === 1 && ( elem.className ?
|
|
( " " + elem.className + " " ).replace( rclass, " " ) :
|
|
" "
|
|
);
|
|
|
|
if ( cur ) {
|
|
j = 0;
|
|
while ( (clazz = classes[j++]) ) {
|
|
if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
|
|
cur += clazz + " ";
|
|
}
|
|
}
|
|
elem.className = jQuery.trim( cur );
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
removeClass: function( value ) {
|
|
var classes, elem, cur, clazz, j,
|
|
i = 0,
|
|
len = this.length,
|
|
proceed = arguments.length === 0 || typeof value === "string" && value;
|
|
|
|
if ( jQuery.isFunction( value ) ) {
|
|
return this.each(function( j ) {
|
|
jQuery( this ).removeClass( value.call( this, j, this.className ) );
|
|
});
|
|
}
|
|
if ( proceed ) {
|
|
classes = ( value || "" ).match( core_rnotwhite ) || [];
|
|
|
|
for ( ; i < len; i++ ) {
|
|
elem = this[ i ];
|
|
// This expression is here for better compressibility (see addClass)
|
|
cur = elem.nodeType === 1 && ( elem.className ?
|
|
( " " + elem.className + " " ).replace( rclass, " " ) :
|
|
""
|
|
);
|
|
|
|
if ( cur ) {
|
|
j = 0;
|
|
while ( (clazz = classes[j++]) ) {
|
|
// Remove *all* instances
|
|
while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {
|
|
cur = cur.replace( " " + clazz + " ", " " );
|
|
}
|
|
}
|
|
elem.className = value ? jQuery.trim( cur ) : "";
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
toggleClass: function( value, stateVal ) {
|
|
var type = typeof value;
|
|
|
|
if ( typeof stateVal === "boolean" && type === "string" ) {
|
|
return stateVal ? this.addClass( value ) : this.removeClass( value );
|
|
}
|
|
|
|
if ( jQuery.isFunction( value ) ) {
|
|
return this.each(function( i ) {
|
|
jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
|
|
});
|
|
}
|
|
|
|
return this.each(function() {
|
|
if ( type === "string" ) {
|
|
// toggle individual class names
|
|
var className,
|
|
i = 0,
|
|
self = jQuery( this ),
|
|
classNames = value.match( core_rnotwhite ) || [];
|
|
|
|
while ( (className = classNames[ i++ ]) ) {
|
|
// check each className given, space separated list
|
|
if ( self.hasClass( className ) ) {
|
|
self.removeClass( className );
|
|
} else {
|
|
self.addClass( className );
|
|
}
|
|
}
|
|
|
|
// Toggle whole class name
|
|
} else if ( type === core_strundefined || type === "boolean" ) {
|
|
if ( this.className ) {
|
|
// store className if set
|
|
jQuery._data( this, "__className__", this.className );
|
|
}
|
|
|
|
// If the element has a class name or if we're passed "false",
|
|
// then remove the whole classname (if there was one, the above saved it).
|
|
// Otherwise bring back whatever was previously saved (if anything),
|
|
// falling back to the empty string if nothing was stored.
|
|
this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
|
|
}
|
|
});
|
|
},
|
|
|
|
hasClass: function( selector ) {
|
|
var className = " " + selector + " ",
|
|
i = 0,
|
|
l = this.length;
|
|
for ( ; i < l; i++ ) {
|
|
if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
val: function( value ) {
|
|
var ret, hooks, isFunction,
|
|
elem = this[0];
|
|
|
|
if ( !arguments.length ) {
|
|
if ( elem ) {
|
|
hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
|
|
|
|
if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
|
|
return ret;
|
|
}
|
|
|
|
ret = elem.value;
|
|
|
|
return typeof ret === "string" ?
|
|
// handle most common string cases
|
|
ret.replace(rreturn, "") :
|
|
// handle cases where value is null/undef or number
|
|
ret == null ? "" : ret;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
isFunction = jQuery.isFunction( value );
|
|
|
|
return this.each(function( i ) {
|
|
var val;
|
|
|
|
if ( this.nodeType !== 1 ) {
|
|
return;
|
|
}
|
|
|
|
if ( isFunction ) {
|
|
val = value.call( this, i, jQuery( this ).val() );
|
|
} else {
|
|
val = value;
|
|
}
|
|
|
|
// Treat null/undefined as ""; convert numbers to string
|
|
if ( val == null ) {
|
|
val = "";
|
|
} else if ( typeof val === "number" ) {
|
|
val += "";
|
|
} else if ( jQuery.isArray( val ) ) {
|
|
val = jQuery.map(val, function ( value ) {
|
|
return value == null ? "" : value + "";
|
|
});
|
|
}
|
|
|
|
hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
|
|
|
|
// If set returns undefined, fall back to normal setting
|
|
if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
|
|
this.value = val;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
jQuery.extend({
|
|
valHooks: {
|
|
option: {
|
|
get: function( elem ) {
|
|
// Use proper attribute retrieval(#6932, #12072)
|
|
var val = jQuery.find.attr( elem, "value" );
|
|
return val != null ?
|
|
val :
|
|
elem.text;
|
|
}
|
|
},
|
|
select: {
|
|
get: function( elem ) {
|
|
var value, option,
|
|
options = elem.options,
|
|
index = elem.selectedIndex,
|
|
one = elem.type === "select-one" || index < 0,
|
|
values = one ? null : [],
|
|
max = one ? index + 1 : options.length,
|
|
i = index < 0 ?
|
|
max :
|
|
one ? index : 0;
|
|
|
|
// Loop through all the selected options
|
|
for ( ; i < max; i++ ) {
|
|
option = options[ i ];
|
|
|
|
// oldIE doesn't update selected after form reset (#2551)
|
|
if ( ( option.selected || i === index ) &&
|
|
// Don't return options that are disabled or in a disabled optgroup
|
|
( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
|
|
( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
|
|
|
|
// Get the specific value for the option
|
|
value = jQuery( option ).val();
|
|
|
|
// We don't need an array for one selects
|
|
if ( one ) {
|
|
return value;
|
|
}
|
|
|
|
// Multi-Selects return an array
|
|
values.push( value );
|
|
}
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
set: function( elem, value ) {
|
|
var optionSet, option,
|
|
options = elem.options,
|
|
values = jQuery.makeArray( value ),
|
|
i = options.length;
|
|
|
|
while ( i-- ) {
|
|
option = options[ i ];
|
|
if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) {
|
|
optionSet = true;
|
|
}
|
|
}
|
|
|
|
// force browsers to behave consistently when non-matching value is set
|
|
if ( !optionSet ) {
|
|
elem.selectedIndex = -1;
|
|
}
|
|
return values;
|
|
}
|
|
}
|
|
},
|
|
|
|
attr: function( elem, name, value ) {
|
|
var hooks, ret,
|
|
nType = elem.nodeType;
|
|
|
|
// don't get/set attributes on text, comment and attribute nodes
|
|
if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
|
|
return;
|
|
}
|
|
|
|
// Fallback to prop when attributes are not supported
|
|
if ( typeof elem.getAttribute === core_strundefined ) {
|
|
return jQuery.prop( elem, name, value );
|
|
}
|
|
|
|
// All attributes are lowercase
|
|
// Grab necessary hook if one is defined
|
|
if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
|
|
name = name.toLowerCase();
|
|
hooks = jQuery.attrHooks[ name ] ||
|
|
( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
|
|
}
|
|
|
|
if ( value !== undefined ) {
|
|
|
|
if ( value === null ) {
|
|
jQuery.removeAttr( elem, name );
|
|
|
|
} else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
|
|
return ret;
|
|
|
|
} else {
|
|
elem.setAttribute( name, value + "" );
|
|
return value;
|
|
}
|
|
|
|
} else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
|
|
return ret;
|
|
|
|
} else {
|
|
ret = jQuery.find.attr( elem, name );
|
|
|
|
// Non-existent attributes return null, we normalize to undefined
|
|
return ret == null ?
|
|
undefined :
|
|
ret;
|
|
}
|
|
},
|
|
|
|
removeAttr: function( elem, value ) {
|
|
var name, propName,
|
|
i = 0,
|
|
attrNames = value && value.match( core_rnotwhite );
|
|
|
|
if ( attrNames && elem.nodeType === 1 ) {
|
|
while ( (name = attrNames[i++]) ) {
|
|
propName = jQuery.propFix[ name ] || name;
|
|
|
|
// Boolean attributes get special treatment (#10870)
|
|
if ( jQuery.expr.match.bool.test( name ) ) {
|
|
// Set corresponding property to false
|
|
if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
|
|
elem[ propName ] = false;
|
|
// Support: IE<9
|
|
// Also clear defaultChecked/defaultSelected (if appropriate)
|
|
} else {
|
|
elem[ jQuery.camelCase( "default-" + name ) ] =
|
|
elem[ propName ] = false;
|
|
}
|
|
|
|
// See #9699 for explanation of this approach (setting first, then removal)
|
|
} else {
|
|
jQuery.attr( elem, name, "" );
|
|
}
|
|
|
|
elem.removeAttribute( getSetAttribute ? name : propName );
|
|
}
|
|
}
|
|
},
|
|
|
|
attrHooks: {
|
|
type: {
|
|
set: function( elem, value ) {
|
|
if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
|
|
// Setting the type on a radio button after the value resets the value in IE6-9
|
|
// Reset value to default in case type is set after value during creation
|
|
var val = elem.value;
|
|
elem.setAttribute( "type", value );
|
|
if ( val ) {
|
|
elem.value = val;
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
propFix: {
|
|
"for": "htmlFor",
|
|
"class": "className"
|
|
},
|
|
|
|
prop: function( elem, name, value ) {
|
|
var ret, hooks, notxml,
|
|
nType = elem.nodeType;
|
|
|
|
// don't get/set properties on text, comment and attribute nodes
|
|
if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
|
|
return;
|
|
}
|
|
|
|
notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
|
|
|
|
if ( notxml ) {
|
|
// Fix name and attach hooks
|
|
name = jQuery.propFix[ name ] || name;
|
|
hooks = jQuery.propHooks[ name ];
|
|
}
|
|
|
|
if ( value !== undefined ) {
|
|
return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?
|
|
ret :
|
|
( elem[ name ] = value );
|
|
|
|
} else {
|
|
return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ?
|
|
ret :
|
|
elem[ name ];
|
|
}
|
|
},
|
|
|
|
propHooks: {
|
|
tabIndex: {
|
|
get: function( elem ) {
|
|
// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
|
|
// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
|
|
// Use proper attribute retrieval(#12072)
|
|
var tabindex = jQuery.find.attr( elem, "tabindex" );
|
|
|
|
return tabindex ?
|
|
parseInt( tabindex, 10 ) :
|
|
rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
|
|
0 :
|
|
-1;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Hooks for boolean attributes
|
|
boolHook = {
|
|
set: function( elem, value, name ) {
|
|
if ( value === false ) {
|
|
// Remove boolean attributes when set to false
|
|
jQuery.removeAttr( elem, name );
|
|
} else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
|
|
// IE<8 needs the *property* name
|
|
elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );
|
|
|
|
// Use defaultChecked and defaultSelected for oldIE
|
|
} else {
|
|
elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true;
|
|
}
|
|
|
|
return name;
|
|
}
|
|
};
|
|
jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
|
|
var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr;
|
|
|
|
jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ?
|
|
function( elem, name, isXML ) {
|
|
var fn = jQuery.expr.attrHandle[ name ],
|
|
ret = isXML ?
|
|
undefined :
|
|
/* jshint eqeqeq: false */
|
|
(jQuery.expr.attrHandle[ name ] = undefined) !=
|
|
getter( elem, name, isXML ) ?
|
|
|
|
name.toLowerCase() :
|
|
null;
|
|
jQuery.expr.attrHandle[ name ] = fn;
|
|
return ret;
|
|
} :
|
|
function( elem, name, isXML ) {
|
|
return isXML ?
|
|
undefined :
|
|
elem[ jQuery.camelCase( "default-" + name ) ] ?
|
|
name.toLowerCase() :
|
|
null;
|
|
};
|
|
});
|
|
|
|
// fix oldIE attroperties
|
|
if ( !getSetInput || !getSetAttribute ) {
|
|
jQuery.attrHooks.value = {
|
|
set: function( elem, value, name ) {
|
|
if ( jQuery.nodeName( elem, "input" ) ) {
|
|
// Does not return so that setAttribute is also used
|
|
elem.defaultValue = value;
|
|
} else {
|
|
// Use nodeHook if defined (#1954); otherwise setAttribute is fine
|
|
return nodeHook && nodeHook.set( elem, value, name );
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// IE6/7 do not support getting/setting some attributes with get/setAttribute
|
|
if ( !getSetAttribute ) {
|
|
|
|
// Use this for any attribute in IE6/7
|
|
// This fixes almost every IE6/7 issue
|
|
nodeHook = {
|
|
set: function( elem, value, name ) {
|
|
// Set the existing or create a new attribute node
|
|
var ret = elem.getAttributeNode( name );
|
|
if ( !ret ) {
|
|
elem.setAttributeNode(
|
|
(ret = elem.ownerDocument.createAttribute( name ))
|
|
);
|
|
}
|
|
|
|
ret.value = value += "";
|
|
|
|
// Break association with cloned elements by also using setAttribute (#9646)
|
|
return name === "value" || value === elem.getAttribute( name ) ?
|
|
value :
|
|
undefined;
|
|
}
|
|
};
|
|
jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords =
|
|
// Some attributes are constructed with empty-string values when not defined
|
|
function( elem, name, isXML ) {
|
|
var ret;
|
|
return isXML ?
|
|
undefined :
|
|
(ret = elem.getAttributeNode( name )) && ret.value !== "" ?
|
|
ret.value :
|
|
null;
|
|
};
|
|
jQuery.valHooks.button = {
|
|
get: function( elem, name ) {
|
|
var ret = elem.getAttributeNode( name );
|
|
return ret && ret.specified ?
|
|
ret.value :
|
|
undefined;
|
|
},
|
|
set: nodeHook.set
|
|
};
|
|
|
|
// Set contenteditable to false on removals(#10429)
|
|
// Setting to empty string throws an error as an invalid value
|
|
jQuery.attrHooks.contenteditable = {
|
|
set: function( elem, value, name ) {
|
|
nodeHook.set( elem, value === "" ? false : value, name );
|
|
}
|
|
};
|
|
|
|
// Set width and height to auto instead of 0 on empty string( Bug #8150 )
|
|
// This is for removals
|
|
jQuery.each([ "width", "height" ], function( i, name ) {
|
|
jQuery.attrHooks[ name ] = {
|
|
set: function( elem, value ) {
|
|
if ( value === "" ) {
|
|
elem.setAttribute( name, "auto" );
|
|
return value;
|
|
}
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
// Some attributes require a special call on IE
|
|
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
|
|
if ( !jQuery.support.hrefNormalized ) {
|
|
// href/src property should get the full normalized URL (#10299/#12915)
|
|
jQuery.each([ "href", "src" ], function( i, name ) {
|
|
jQuery.propHooks[ name ] = {
|
|
get: function( elem ) {
|
|
return elem.getAttribute( name, 4 );
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
if ( !jQuery.support.style ) {
|
|
jQuery.attrHooks.style = {
|
|
get: function( elem ) {
|
|
// Return undefined in the case of empty string
|
|
// Note: IE uppercases css property names, but if we were to .toLowerCase()
|
|
// .cssText, that would destroy case senstitivity in URL's, like in "background"
|
|
return elem.style.cssText || undefined;
|
|
},
|
|
set: function( elem, value ) {
|
|
return ( elem.style.cssText = value + "" );
|
|
}
|
|
};
|
|
}
|
|
|
|
// Safari mis-reports the default selected property of an option
|
|
// Accessing the parent's selectedIndex property fixes it
|
|
if ( !jQuery.support.optSelected ) {
|
|
jQuery.propHooks.selected = {
|
|
get: function( elem ) {
|
|
var parent = elem.parentNode;
|
|
|
|
if ( parent ) {
|
|
parent.selectedIndex;
|
|
|
|
// Make sure that it also works with optgroups, see #5701
|
|
if ( parent.parentNode ) {
|
|
parent.parentNode.selectedIndex;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
}
|
|
|
|
jQuery.each([
|
|
"tabIndex",
|
|
"readOnly",
|
|
"maxLength",
|
|
"cellSpacing",
|
|
"cellPadding",
|
|
"rowSpan",
|
|
"colSpan",
|
|
"useMap",
|
|
"frameBorder",
|
|
"contentEditable"
|
|
], function() {
|
|
jQuery.propFix[ this.toLowerCase() ] = this;
|
|
});
|
|
|
|
// IE6/7 call enctype encoding
|
|
if ( !jQuery.support.enctype ) {
|
|
jQuery.propFix.enctype = "encoding";
|
|
}
|
|
|
|
// Radios and checkboxes getter/setter
|
|
jQuery.each([ "radio", "checkbox" ], function() {
|
|
jQuery.valHooks[ this ] = {
|
|
set: function( elem, value ) {
|
|
if ( jQuery.isArray( value ) ) {
|
|
return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
|
|
}
|
|
}
|
|
};
|
|
if ( !jQuery.support.checkOn ) {
|
|
jQuery.valHooks[ this ].get = function( elem ) {
|
|
// Support: Webkit
|
|
// "" is returned instead of "on" if a value isn't specified
|
|
return elem.getAttribute("value") === null ? "on" : elem.value;
|
|
};
|
|
}
|
|
});
|
|
var rformElems = /^(?:input|select|textarea)$/i,
|
|
rkeyEvent = /^key/,
|
|
rmouseEvent = /^(?:mouse|contextmenu)|click/,
|
|
rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
|
|
rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
|
|
|
|
function returnTrue() {
|
|
return true;
|
|
}
|
|
|
|
function returnFalse() {
|
|
return false;
|
|
}
|
|
|
|
function safeActiveElement() {
|
|
try {
|
|
return document.activeElement;
|
|
} catch ( err ) { }
|
|
}
|
|
|
|
/*
|
|
* Helper functions for managing events -- not part of the public interface.
|
|
* Props to Dean Edwards' addEvent library for many of the ideas.
|
|
*/
|
|
jQuery.event = {
|
|
|
|
global: {},
|
|
|
|
add: function( elem, types, handler, data, selector ) {
|
|
var tmp, events, t, handleObjIn,
|
|
special, eventHandle, handleObj,
|
|
handlers, type, namespaces, origType,
|
|
elemData = jQuery._data( elem );
|
|
|
|
// Don't attach events to noData or text/comment nodes (but allow plain objects)
|
|
if ( !elemData ) {
|
|
return;
|
|
}
|
|
|
|
// Caller can pass in an object of custom data in lieu of the handler
|
|
if ( handler.handler ) {
|
|
handleObjIn = handler;
|
|
handler = handleObjIn.handler;
|
|
selector = handleObjIn.selector;
|
|
}
|
|
|
|
// Make sure that the handler has a unique ID, used to find/remove it later
|
|
if ( !handler.guid ) {
|
|
handler.guid = jQuery.guid++;
|
|
}
|
|
|
|
// Init the element's event structure and main handler, if this is the first
|
|
if ( !(events = elemData.events) ) {
|
|
events = elemData.events = {};
|
|
}
|
|
if ( !(eventHandle = elemData.handle) ) {
|
|
eventHandle = elemData.handle = function( e ) {
|
|
// Discard the second event of a jQuery.event.trigger() and
|
|
// when an event is called after a page has unloaded
|
|
return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
|
|
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
|
|
undefined;
|
|
};
|
|
// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
|
|
eventHandle.elem = elem;
|
|
}
|
|
|
|
// Handle multiple events separated by a space
|
|
types = ( types || "" ).match( core_rnotwhite ) || [""];
|
|
t = types.length;
|
|
while ( t-- ) {
|
|
tmp = rtypenamespace.exec( types[t] ) || [];
|
|
type = origType = tmp[1];
|
|
namespaces = ( tmp[2] || "" ).split( "." ).sort();
|
|
|
|
// There *must* be a type, no attaching namespace-only handlers
|
|
if ( !type ) {
|
|
continue;
|
|
}
|
|
|
|
// If event changes its type, use the special event handlers for the changed type
|
|
special = jQuery.event.special[ type ] || {};
|
|
|
|
// If selector defined, determine special event api type, otherwise given type
|
|
type = ( selector ? special.delegateType : special.bindType ) || type;
|
|
|
|
// Update special based on newly reset type
|
|
special = jQuery.event.special[ type ] || {};
|
|
|
|
// handleObj is passed to all event handlers
|
|
handleObj = jQuery.extend({
|
|
type: type,
|
|
origType: origType,
|
|
data: data,
|
|
handler: handler,
|
|
guid: handler.guid,
|
|
selector: selector,
|
|
needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
|
|
namespace: namespaces.join(".")
|
|
}, handleObjIn );
|
|
|
|
// Init the event handler queue if we're the first
|
|
if ( !(handlers = events[ type ]) ) {
|
|
handlers = events[ type ] = [];
|
|
handlers.delegateCount = 0;
|
|
|
|
// Only use addEventListener/attachEvent if the special events handler returns false
|
|
if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
|
|
// Bind the global event handler to the element
|
|
if ( elem.addEventListener ) {
|
|
elem.addEventListener( type, eventHandle, false );
|
|
|
|
} else if ( elem.attachEvent ) {
|
|
elem.attachEvent( "on" + type, eventHandle );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( special.add ) {
|
|
special.add.call( elem, handleObj );
|
|
|
|
if ( !handleObj.handler.guid ) {
|
|
handleObj.handler.guid = handler.guid;
|
|
}
|
|
}
|
|
|
|
// Add to the element's handler list, delegates in front
|
|
if ( selector ) {
|
|
handlers.splice( handlers.delegateCount++, 0, handleObj );
|
|
} else {
|
|
handlers.push( handleObj );
|
|
}
|
|
|
|
// Keep track of which events have ever been used, for event optimization
|
|
jQuery.event.global[ type ] = true;
|
|
}
|
|
|
|
// Nullify elem to prevent memory leaks in IE
|
|
elem = null;
|
|
},
|
|
|
|
// Detach an event or set of events from an element
|
|
remove: function( elem, types, handler, selector, mappedTypes ) {
|
|
var j, handleObj, tmp,
|
|
origCount, t, events,
|
|
special, handlers, type,
|
|
namespaces, origType,
|
|
elemData = jQuery.hasData( elem ) && jQuery._data( elem );
|
|
|
|
if ( !elemData || !(events = elemData.events) ) {
|
|
return;
|
|
}
|
|
|
|
// Once for each type.namespace in types; type may be omitted
|
|
types = ( types || "" ).match( core_rnotwhite ) || [""];
|
|
t = types.length;
|
|
while ( t-- ) {
|
|
tmp = rtypenamespace.exec( types[t] ) || [];
|
|
type = origType = tmp[1];
|
|
namespaces = ( tmp[2] || "" ).split( "." ).sort();
|
|
|
|
// Unbind all events (on this namespace, if provided) for the element
|
|
if ( !type ) {
|
|
for ( type in events ) {
|
|
jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
special = jQuery.event.special[ type ] || {};
|
|
type = ( selector ? special.delegateType : special.bindType ) || type;
|
|
handlers = events[ type ] || [];
|
|
tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" );
|
|
|
|
// Remove matching events
|
|
origCount = j = handlers.length;
|
|
while ( j-- ) {
|
|
handleObj = handlers[ j ];
|
|
|
|
if ( ( mappedTypes || origType === handleObj.origType ) &&
|
|
( !handler || handler.guid === handleObj.guid ) &&
|
|
( !tmp || tmp.test( handleObj.namespace ) ) &&
|
|
( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
|
|
handlers.splice( j, 1 );
|
|
|
|
if ( handleObj.selector ) {
|
|
handlers.delegateCount--;
|
|
}
|
|
if ( special.remove ) {
|
|
special.remove.call( elem, handleObj );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove generic event handler if we removed something and no more handlers exist
|
|
// (avoids potential for endless recursion during removal of special event handlers)
|
|
if ( origCount && !handlers.length ) {
|
|
if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
|
|
jQuery.removeEvent( elem, type, elemData.handle );
|
|
}
|
|
|
|
delete events[ type ];
|
|
}
|
|
}
|
|
|
|
// Remove the expando if it's no longer used
|
|
if ( jQuery.isEmptyObject( events ) ) {
|
|
delete elemData.handle;
|
|
|
|
// removeData also checks for emptiness and clears the expando if empty
|
|
// so use it instead of delete
|
|
jQuery._removeData( elem, "events" );
|
|
}
|
|
},
|
|
|
|
trigger: function( event, data, elem, onlyHandlers ) {
|
|
var handle, ontype, cur,
|
|
bubbleType, special, tmp, i,
|
|
eventPath = [ elem || document ],
|
|
type = core_hasOwn.call( event, "type" ) ? event.type : event,
|
|
namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : [];
|
|
|
|
cur = tmp = elem = elem || document;
|
|
|
|
// Don't do events on text and comment nodes
|
|
if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
|
|
return;
|
|
}
|
|
|
|
// focus/blur morphs to focusin/out; ensure we're not firing them right now
|
|
if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( type.indexOf(".") >= 0 ) {
|
|
// Namespaced trigger; create a regexp to match event type in handle()
|
|
namespaces = type.split(".");
|
|
type = namespaces.shift();
|
|
namespaces.sort();
|
|
}
|
|
ontype = type.indexOf(":") < 0 && "on" + type;
|
|
|
|
// Caller can pass in a jQuery.Event object, Object, or just an event type string
|
|
event = event[ jQuery.expando ] ?
|
|
event :
|
|
new jQuery.Event( type, typeof event === "object" && event );
|
|
|
|
// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
|
|
event.isTrigger = onlyHandlers ? 2 : 3;
|
|
event.namespace = namespaces.join(".");
|
|
event.namespace_re = event.namespace ?
|
|
new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
|
|
null;
|
|
|
|
// Clean up the event in case it is being reused
|
|
event.result = undefined;
|
|
if ( !event.target ) {
|
|
event.target = elem;
|
|
}
|
|
|
|
// Clone any incoming data and prepend the event, creating the handler arg list
|
|
data = data == null ?
|
|
[ event ] :
|
|
jQuery.makeArray( data, [ event ] );
|
|
|
|
// Allow special events to draw outside the lines
|
|
special = jQuery.event.special[ type ] || {};
|
|
if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
|
|
return;
|
|
}
|
|
|
|
// Determine event propagation path in advance, per W3C events spec (#9951)
|
|
// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
|
|
if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
|
|
|
|
bubbleType = special.delegateType || type;
|
|
if ( !rfocusMorph.test( bubbleType + type ) ) {
|
|
cur = cur.parentNode;
|
|
}
|
|
for ( ; cur; cur = cur.parentNode ) {
|
|
eventPath.push( cur );
|
|
tmp = cur;
|
|
}
|
|
|
|
// Only add window if we got to document (e.g., not plain obj or detached DOM)
|
|
if ( tmp === (elem.ownerDocument || document) ) {
|
|
eventPath.push( tmp.defaultView || tmp.parentWindow || window );
|
|
}
|
|
}
|
|
|
|
// Fire handlers on the event path
|
|
i = 0;
|
|
while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {
|
|
|
|
event.type = i > 1 ?
|
|
bubbleType :
|
|
special.bindType || type;
|
|
|
|
// jQuery handler
|
|
handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
|
|
if ( handle ) {
|
|
handle.apply( cur, data );
|
|
}
|
|
|
|
// Native handler
|
|
handle = ontype && cur[ ontype ];
|
|
if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
event.type = type;
|
|
|
|
// If nobody prevented the default action, do it now
|
|
if ( !onlyHandlers && !event.isDefaultPrevented() ) {
|
|
|
|
if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&
|
|
jQuery.acceptData( elem ) ) {
|
|
|
|
// Call a native DOM method on the target with the same name name as the event.
|
|
// Can't use an .isFunction() check here because IE6/7 fails that test.
|
|
// Don't do default actions on window, that's where global variables be (#6170)
|
|
if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) {
|
|
|
|
// Don't re-trigger an onFOO event when we call its FOO() method
|
|
tmp = elem[ ontype ];
|
|
|
|
if ( tmp ) {
|
|
elem[ ontype ] = null;
|
|
}
|
|
|
|
// Prevent re-triggering of the same event, since we already bubbled it above
|
|
jQuery.event.triggered = type;
|
|
try {
|
|
elem[ type ]();
|
|
} catch ( e ) {
|
|
// IE<9 dies on focus/blur to hidden element (#1486,#12518)
|
|
// only reproducible on winXP IE8 native, not IE9 in IE8 mode
|
|
}
|
|
jQuery.event.triggered = undefined;
|
|
|
|
if ( tmp ) {
|
|
elem[ ontype ] = tmp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return event.result;
|
|
},
|
|
|
|
dispatch: function( event ) {
|
|
|
|
// Make a writable jQuery.Event from the native event object
|
|
event = jQuery.event.fix( event );
|
|
|
|
var i, ret, handleObj, matched, j,
|
|
handlerQueue = [],
|
|
args = core_slice.call( arguments ),
|
|
handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
|
|
special = jQuery.event.special[ event.type ] || {};
|
|
|
|
// Use the fix-ed jQuery.Event rather than the (read-only) native event
|
|
args[0] = event;
|
|
event.delegateTarget = this;
|
|
|
|
// Call the preDispatch hook for the mapped type, and let it bail if desired
|
|
if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
|
|
return;
|
|
}
|
|
|
|
// Determine handlers
|
|
handlerQueue = jQuery.event.handlers.call( this, event, handlers );
|
|
|
|
// Run delegates first; they may want to stop propagation beneath us
|
|
i = 0;
|
|
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
|
|
event.currentTarget = matched.elem;
|
|
|
|
j = 0;
|
|
while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
|
|
|
|
// Triggered event must either 1) have no namespace, or
|
|
// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
|
|
if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
|
|
|
|
event.handleObj = handleObj;
|
|
event.data = handleObj.data;
|
|
|
|
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
|
|
.apply( matched.elem, args );
|
|
|
|
if ( ret !== undefined ) {
|
|
if ( (event.result = ret) === false ) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call the postDispatch hook for the mapped type
|
|
if ( special.postDispatch ) {
|
|
special.postDispatch.call( this, event );
|
|
}
|
|
|
|
return event.result;
|
|
},
|
|
|
|
handlers: function( event, handlers ) {
|
|
var sel, handleObj, matches, i,
|
|
handlerQueue = [],
|
|
delegateCount = handlers.delegateCount,
|
|
cur = event.target;
|
|
|
|
// Find delegate handlers
|
|
// Black-hole SVG <use> instance trees (#13180)
|
|
// Avoid non-left-click bubbling in Firefox (#3861)
|
|
if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
|
|
|
|
/* jshint eqeqeq: false */
|
|
for ( ; cur != this; cur = cur.parentNode || this ) {
|
|
/* jshint eqeqeq: true */
|
|
|
|
// Don't check non-elements (#13208)
|
|
// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
|
|
if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) {
|
|
matches = [];
|
|
for ( i = 0; i < delegateCount; i++ ) {
|
|
handleObj = handlers[ i ];
|
|
|
|
// Don't conflict with Object.prototype properties (#13203)
|
|
sel = handleObj.selector + " ";
|
|
|
|
if ( matches[ sel ] === undefined ) {
|
|
matches[ sel ] = handleObj.needsContext ?
|
|
jQuery( sel, this ).index( cur ) >= 0 :
|
|
jQuery.find( sel, this, null, [ cur ] ).length;
|
|
}
|
|
if ( matches[ sel ] ) {
|
|
matches.push( handleObj );
|
|
}
|
|
}
|
|
if ( matches.length ) {
|
|
handlerQueue.push({ elem: cur, handlers: matches });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the remaining (directly-bound) handlers
|
|
if ( delegateCount < handlers.length ) {
|
|
handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
|
|
}
|
|
|
|
return handlerQueue;
|
|
},
|
|
|
|
fix: function( event ) {
|
|
if ( event[ jQuery.expando ] ) {
|
|
return event;
|
|
}
|
|
|
|
// Create a writable copy of the event object and normalize some properties
|
|
var i, prop, copy,
|
|
type = event.type,
|
|
originalEvent = event,
|
|
fixHook = this.fixHooks[ type ];
|
|
|
|
if ( !fixHook ) {
|
|
this.fixHooks[ type ] = fixHook =
|
|
rmouseEvent.test( type ) ? this.mouseHooks :
|
|
rkeyEvent.test( type ) ? this.keyHooks :
|
|
{};
|
|
}
|
|
copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
|
|
|
|
event = new jQuery.Event( originalEvent );
|
|
|
|
i = copy.length;
|
|
while ( i-- ) {
|
|
prop = copy[ i ];
|
|
event[ prop ] = originalEvent[ prop ];
|
|
}
|
|
|
|
// Support: IE<9
|
|
// Fix target property (#1925)
|
|
if ( !event.target ) {
|
|
event.target = originalEvent.srcElement || document;
|
|
}
|
|
|
|
// Support: Chrome 23+, Safari?
|
|
// Target should not be a text node (#504, #13143)
|
|
if ( event.target.nodeType === 3 ) {
|
|
event.target = event.target.parentNode;
|
|
}
|
|
|
|
// Support: IE<9
|
|
// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
|
|
event.metaKey = !!event.metaKey;
|
|
|
|
return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
|
|
},
|
|
|
|
// Includes some event props shared by KeyEvent and MouseEvent
|
|
props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
|
|
|
|
fixHooks: {},
|
|
|
|
keyHooks: {
|
|
props: "char charCode key keyCode".split(" "),
|
|
filter: function( event, original ) {
|
|
|
|
// Add which for key events
|
|
if ( event.which == null ) {
|
|
event.which = original.charCode != null ? original.charCode : original.keyCode;
|
|
}
|
|
|
|
return event;
|
|
}
|
|
},
|
|
|
|
mouseHooks: {
|
|
props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
|
|
filter: function( event, original ) {
|
|
var body, eventDoc, doc,
|
|
button = original.button,
|
|
fromElement = original.fromElement;
|
|
|
|
// Calculate pageX/Y if missing and clientX/Y available
|
|
if ( event.pageX == null && original.clientX != null ) {
|
|
eventDoc = event.target.ownerDocument || document;
|
|
doc = eventDoc.documentElement;
|
|
body = eventDoc.body;
|
|
|
|
event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
|
|
event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 );
|
|
}
|
|
|
|
// Add relatedTarget, if necessary
|
|
if ( !event.relatedTarget && fromElement ) {
|
|
event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
|
|
}
|
|
|
|
// Add which for click: 1 === left; 2 === middle; 3 === right
|
|
// Note: button is not normalized, so don't use it
|
|
if ( !event.which && button !== undefined ) {
|
|
event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
|
|
}
|
|
|
|
return event;
|
|
}
|
|
},
|
|
|
|
special: {
|
|
load: {
|
|
// Prevent triggered image.load events from bubbling to window.load
|
|
noBubble: true
|
|
},
|
|
focus: {
|
|
// Fire native event if possible so blur/focus sequence is correct
|
|
trigger: function() {
|
|
if ( this !== safeActiveElement() && this.focus ) {
|
|
try {
|
|
this.focus();
|
|
return false;
|
|
} catch ( e ) {
|
|
// Support: IE<9
|
|
// If we error on focus to hidden element (#1486, #12518),
|
|
// let .trigger() run the handlers
|
|
}
|
|
}
|
|
},
|
|
delegateType: "focusin"
|
|
},
|
|
blur: {
|
|
trigger: function() {
|
|
if ( this === safeActiveElement() && this.blur ) {
|
|
this.blur();
|
|
return false;
|
|
}
|
|
},
|
|
delegateType: "focusout"
|
|
},
|
|
click: {
|
|
// For checkbox, fire native event so checked state will be right
|
|
trigger: function() {
|
|
if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) {
|
|
this.click();
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// For cross-browser consistency, don't fire native .click() on links
|
|
_default: function( event ) {
|
|
return jQuery.nodeName( event.target, "a" );
|
|
}
|
|
},
|
|
|
|
beforeunload: {
|
|
postDispatch: function( event ) {
|
|
|
|
// Even when returnValue equals to undefined Firefox will still show alert
|
|
if ( event.result !== undefined ) {
|
|
event.originalEvent.returnValue = event.result;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
simulate: function( type, elem, event, bubble ) {
|
|
// Piggyback on a donor event to simulate a different one.
|
|
// Fake originalEvent to avoid donor's stopPropagation, but if the
|
|
// simulated event prevents default then we do the same on the donor.
|
|
var e = jQuery.extend(
|
|
new jQuery.Event(),
|
|
event,
|
|
{
|
|
type: type,
|
|
isSimulated: true,
|
|
originalEvent: {}
|
|
}
|
|
);
|
|
if ( bubble ) {
|
|
jQuery.event.trigger( e, null, elem );
|
|
} else {
|
|
jQuery.event.dispatch.call( elem, e );
|
|
}
|
|
if ( e.isDefaultPrevented() ) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
};
|
|
|
|
jQuery.removeEvent = document.removeEventListener ?
|
|
function( elem, type, handle ) {
|
|
if ( elem.removeEventListener ) {
|
|
elem.removeEventListener( type, handle, false );
|
|
}
|
|
} :
|
|
function( elem, type, handle ) {
|
|
var name = "on" + type;
|
|
|
|
if ( elem.detachEvent ) {
|
|
|
|
// #8545, #7054, preventing memory leaks for custom events in IE6-8
|
|
// detachEvent needed property on element, by name of that event, to properly expose it to GC
|
|
if ( typeof elem[ name ] === core_strundefined ) {
|
|
elem[ name ] = null;
|
|
}
|
|
|
|
elem.detachEvent( name, handle );
|
|
}
|
|
};
|
|
|
|
jQuery.Event = function( src, props ) {
|
|
// Allow instantiation without the 'new' keyword
|
|
if ( !(this instanceof jQuery.Event) ) {
|
|
return new jQuery.Event( src, props );
|
|
}
|
|
|
|
// Event object
|
|
if ( src && src.type ) {
|
|
this.originalEvent = src;
|
|
this.type = src.type;
|
|
|
|
// Events bubbling up the document may have been marked as prevented
|
|
// by a handler lower down the tree; reflect the correct value.
|
|
this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
|
|
src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
|
|
|
|
// Event type
|
|
} else {
|
|
this.type = src;
|
|
}
|
|
|
|
// Put explicitly provided properties onto the event object
|
|
if ( props ) {
|
|
jQuery.extend( this, props );
|
|
}
|
|
|
|
// Create a timestamp if incoming event doesn't have one
|
|
this.timeStamp = src && src.timeStamp || jQuery.now();
|
|
|
|
// Mark it as fixed
|
|
this[ jQuery.expando ] = true;
|
|
};
|
|
|
|
// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
|
|
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
|
|
jQuery.Event.prototype = {
|
|
isDefaultPrevented: returnFalse,
|
|
isPropagationStopped: returnFalse,
|
|
isImmediatePropagationStopped: returnFalse,
|
|
|
|
preventDefault: function() {
|
|
var e = this.originalEvent;
|
|
|
|
this.isDefaultPrevented = returnTrue;
|
|
if ( !e ) {
|
|
return;
|
|
}
|
|
|
|
// If preventDefault exists, run it on the original event
|
|
if ( e.preventDefault ) {
|
|
e.preventDefault();
|
|
|
|
// Support: IE
|
|
// Otherwise set the returnValue property of the original event to false
|
|
} else {
|
|
e.returnValue = false;
|
|
}
|
|
},
|
|
stopPropagation: function() {
|
|
var e = this.originalEvent;
|
|
|
|
this.isPropagationStopped = returnTrue;
|
|
if ( !e ) {
|
|
return;
|
|
}
|
|
// If stopPropagation exists, run it on the original event
|
|
if ( e.stopPropagation ) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Support: IE
|
|
// Set the cancelBubble property of the original event to true
|
|
e.cancelBubble = true;
|
|
},
|
|
stopImmediatePropagation: function() {
|
|
this.isImmediatePropagationStopped = returnTrue;
|
|
this.stopPropagation();
|
|
}
|
|
};
|
|
|
|
// Create mouseenter/leave events using mouseover/out and event-time checks
|
|
jQuery.each({
|
|
mouseenter: "mouseover",
|
|
mouseleave: "mouseout"
|
|
}, function( orig, fix ) {
|
|
jQuery.event.special[ orig ] = {
|
|
delegateType: fix,
|
|
bindType: fix,
|
|
|
|
handle: function( event ) {
|
|
var ret,
|
|
target = this,
|
|
related = event.relatedTarget,
|
|
handleObj = event.handleObj;
|
|
|
|
// For mousenter/leave call the handler if related is outside the target.
|
|
// NB: No relatedTarget if the mouse left/entered the browser window
|
|
if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
|
|
event.type = handleObj.origType;
|
|
ret = handleObj.handler.apply( this, arguments );
|
|
event.type = fix;
|
|
}
|
|
return ret;
|
|
}
|
|
};
|
|
});
|
|
|
|
// IE submit delegation
|
|
if ( !jQuery.support.submitBubbles ) {
|
|
|
|
jQuery.event.special.submit = {
|
|
setup: function() {
|
|
// Only need this for delegated form submit events
|
|
if ( jQuery.nodeName( this, "form" ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Lazy-add a submit handler when a descendant form may potentially be submitted
|
|
jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
|
|
// Node name check avoids a VML-related crash in IE (#9807)
|
|
var elem = e.target,
|
|
form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
|
|
if ( form && !jQuery._data( form, "submitBubbles" ) ) {
|
|
jQuery.event.add( form, "submit._submit", function( event ) {
|
|
event._submit_bubble = true;
|
|
});
|
|
jQuery._data( form, "submitBubbles", true );
|
|
}
|
|
});
|
|
// return undefined since we don't need an event listener
|
|
},
|
|
|
|
postDispatch: function( event ) {
|
|
// If form was submitted by the user, bubble the event up the tree
|
|
if ( event._submit_bubble ) {
|
|
delete event._submit_bubble;
|
|
if ( this.parentNode && !event.isTrigger ) {
|
|
jQuery.event.simulate( "submit", this.parentNode, event, true );
|
|
}
|
|
}
|
|
},
|
|
|
|
teardown: function() {
|
|
// Only need this for delegated form submit events
|
|
if ( jQuery.nodeName( this, "form" ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
|
|
jQuery.event.remove( this, "._submit" );
|
|
}
|
|
};
|
|
}
|
|
|
|
// IE change delegation and checkbox/radio fix
|
|
if ( !jQuery.support.changeBubbles ) {
|
|
|
|
jQuery.event.special.change = {
|
|
|
|
setup: function() {
|
|
|
|
if ( rformElems.test( this.nodeName ) ) {
|
|
// IE doesn't fire change on a check/radio until blur; trigger it on click
|
|
// after a propertychange. Eat the blur-change in special.change.handle.
|
|
// This still fires onchange a second time for check/radio after blur.
|
|
if ( this.type === "checkbox" || this.type === "radio" ) {
|
|
jQuery.event.add( this, "propertychange._change", function( event ) {
|
|
if ( event.originalEvent.propertyName === "checked" ) {
|
|
this._just_changed = true;
|
|
}
|
|
});
|
|
jQuery.event.add( this, "click._change", function( event ) {
|
|
if ( this._just_changed && !event.isTrigger ) {
|
|
this._just_changed = false;
|
|
}
|
|
// Allow triggered, simulated change events (#11500)
|
|
jQuery.event.simulate( "change", this, event, true );
|
|
});
|
|
}
|
|
return false;
|
|
}
|
|
// Delegated event; lazy-add a change handler on descendant inputs
|
|
jQuery.event.add( this, "beforeactivate._change", function( e ) {
|
|
var elem = e.target;
|
|
|
|
if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) {
|
|
jQuery.event.add( elem, "change._change", function( event ) {
|
|
if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
|
|
jQuery.event.simulate( "change", this.parentNode, event, true );
|
|
}
|
|
});
|
|
jQuery._data( elem, "changeBubbles", true );
|
|
}
|
|
});
|
|
},
|
|
|
|
handle: function( event ) {
|
|
var elem = event.target;
|
|
|
|
// Swallow native change events from checkbox/radio, we already triggered them above
|
|
if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
|
|
return event.handleObj.handler.apply( this, arguments );
|
|
}
|
|
},
|
|
|
|
teardown: function() {
|
|
jQuery.event.remove( this, "._change" );
|
|
|
|
return !rformElems.test( this.nodeName );
|
|
}
|
|
};
|
|
}
|
|
|
|
// Create "bubbling" focus and blur events
|
|
if ( !jQuery.support.focusinBubbles ) {
|
|
jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
|
|
|
|
// Attach a single capturing handler while someone wants focusin/focusout
|
|
var attaches = 0,
|
|
handler = function( event ) {
|
|
jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
|
|
};
|
|
|
|
jQuery.event.special[ fix ] = {
|
|
setup: function() {
|
|
if ( attaches++ === 0 ) {
|
|
document.addEventListener( orig, handler, true );
|
|
}
|
|
},
|
|
teardown: function() {
|
|
if ( --attaches === 0 ) {
|
|
document.removeEventListener( orig, handler, true );
|
|
}
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
jQuery.fn.extend({
|
|
|
|
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
|
|
var type, origFn;
|
|
|
|
// Types can be a map of types/handlers
|
|
if ( typeof types === "object" ) {
|
|
// ( types-Object, selector, data )
|
|
if ( typeof selector !== "string" ) {
|
|
// ( types-Object, data )
|
|
data = data || selector;
|
|
selector = undefined;
|
|
}
|
|
for ( type in types ) {
|
|
this.on( type, selector, data, types[ type ], one );
|
|
}
|
|
return this;
|
|
}
|
|
|
|
if ( data == null && fn == null ) {
|
|
// ( types, fn )
|
|
fn = selector;
|
|
data = selector = undefined;
|
|
} else if ( fn == null ) {
|
|
if ( typeof selector === "string" ) {
|
|
// ( types, selector, fn )
|
|
fn = data;
|
|
data = undefined;
|
|
} else {
|
|
// ( types, data, fn )
|
|
fn = data;
|
|
data = selector;
|
|
selector = undefined;
|
|
}
|
|
}
|
|
if ( fn === false ) {
|
|
fn = returnFalse;
|
|
} else if ( !fn ) {
|
|
return this;
|
|
}
|
|
|
|
if ( one === 1 ) {
|
|
origFn = fn;
|
|
fn = function( event ) {
|
|
// Can use an empty set, since event contains the info
|
|
jQuery().off( event );
|
|
return origFn.apply( this, arguments );
|
|
};
|
|
// Use same guid so caller can remove using origFn
|
|
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
|
|
}
|
|
return this.each( function() {
|
|
jQuery.event.add( this, types, fn, data, selector );
|
|
});
|
|
},
|
|
one: function( types, selector, data, fn ) {
|
|
return this.on( types, selector, data, fn, 1 );
|
|
},
|
|
off: function( types, selector, fn ) {
|
|
var handleObj, type;
|
|
if ( types && types.preventDefault && types.handleObj ) {
|
|
// ( event ) dispatched jQuery.Event
|
|
handleObj = types.handleObj;
|
|
jQuery( types.delegateTarget ).off(
|
|
handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
|
|
handleObj.selector,
|
|
handleObj.handler
|
|
);
|
|
return this;
|
|
}
|
|
if ( typeof types === "object" ) {
|
|
// ( types-object [, selector] )
|
|
for ( type in types ) {
|
|
this.off( type, selector, types[ type ] );
|
|
}
|
|
return this;
|
|
}
|
|
if ( selector === false || typeof selector === "function" ) {
|
|
// ( types [, fn] )
|
|
fn = selector;
|
|
selector = undefined;
|
|
}
|
|
if ( fn === false ) {
|
|
fn = returnFalse;
|
|
}
|
|
return this.each(function() {
|
|
jQuery.event.remove( this, types, fn, selector );
|
|
});
|
|
},
|
|
|
|
trigger: function( type, data ) {
|
|
return this.each(function() {
|
|
jQuery.event.trigger( type, data, this );
|
|
});
|
|
},
|
|
triggerHandler: function( type, data ) {
|
|
var elem = this[0];
|
|
if ( elem ) {
|
|
return jQuery.event.trigger( type, data, elem, true );
|
|
}
|
|
}
|
|
});
|
|
var isSimple = /^.[^:#\[\.,]*$/,
|
|
rparentsprev = /^(?:parents|prev(?:Until|All))/,
|
|
rneedsContext = jQuery.expr.match.needsContext,
|
|
// methods guaranteed to produce a unique set when starting from a unique set
|
|
guaranteedUnique = {
|
|
children: true,
|
|
contents: true,
|
|
next: true,
|
|
prev: true
|
|
};
|
|
|
|
jQuery.fn.extend({
|
|
find: function( selector ) {
|
|
var i,
|
|
ret = [],
|
|
self = this,
|
|
len = self.length;
|
|
|
|
if ( typeof selector !== "string" ) {
|
|
return this.pushStack( jQuery( selector ).filter(function() {
|
|
for ( i = 0; i < len; i++ ) {
|
|
if ( jQuery.contains( self[ i ], this ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
}) );
|
|
}
|
|
|
|
for ( i = 0; i < len; i++ ) {
|
|
jQuery.find( selector, self[ i ], ret );
|
|
}
|
|
|
|
// Needed because $( selector, context ) becomes $( context ).find( selector )
|
|
ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
|
|
ret.selector = this.selector ? this.selector + " " + selector : selector;
|
|
return ret;
|
|
},
|
|
|
|
has: function( target ) {
|
|
var i,
|
|
targets = jQuery( target, this ),
|
|
len = targets.length;
|
|
|
|
return this.filter(function() {
|
|
for ( i = 0; i < len; i++ ) {
|
|
if ( jQuery.contains( this, targets[i] ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
not: function( selector ) {
|
|
return this.pushStack( winnow(this, selector || [], true) );
|
|
},
|
|
|
|
filter: function( selector ) {
|
|
return this.pushStack( winnow(this, selector || [], false) );
|
|
},
|
|
|
|
is: function( selector ) {
|
|
return !!winnow(
|
|
this,
|
|
|
|
// If this is a positional/relative selector, check membership in the returned set
|
|
// so $("p:first").is("p:last") won't return true for a doc with two "p".
|
|
typeof selector === "string" && rneedsContext.test( selector ) ?
|
|
jQuery( selector ) :
|
|
selector || [],
|
|
false
|
|
).length;
|
|
},
|
|
|
|
closest: function( selectors, context ) {
|
|
var cur,
|
|
i = 0,
|
|
l = this.length,
|
|
ret = [],
|
|
pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
|
|
jQuery( selectors, context || this.context ) :
|
|
0;
|
|
|
|
for ( ; i < l; i++ ) {
|
|
for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {
|
|
// Always skip document fragments
|
|
if ( cur.nodeType < 11 && (pos ?
|
|
pos.index(cur) > -1 :
|
|
|
|
// Don't pass non-elements to Sizzle
|
|
cur.nodeType === 1 &&
|
|
jQuery.find.matchesSelector(cur, selectors)) ) {
|
|
|
|
cur = ret.push( cur );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret );
|
|
},
|
|
|
|
// Determine the position of an element within
|
|
// the matched set of elements
|
|
index: function( elem ) {
|
|
|
|
// No argument, return index in parent
|
|
if ( !elem ) {
|
|
return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1;
|
|
}
|
|
|
|
// index in selector
|
|
if ( typeof elem === "string" ) {
|
|
return jQuery.inArray( this[0], jQuery( elem ) );
|
|
}
|
|
|
|
// Locate the position of the desired element
|
|
return jQuery.inArray(
|
|
// If it receives a jQuery object, the first element is used
|
|
elem.jquery ? elem[0] : elem, this );
|
|
},
|
|
|
|
add: function( selector, context ) {
|
|
var set = typeof selector === "string" ?
|
|
jQuery( selector, context ) :
|
|
jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
|
|
all = jQuery.merge( this.get(), set );
|
|
|
|
return this.pushStack( jQuery.unique(all) );
|
|
},
|
|
|
|
addBack: function( selector ) {
|
|
return this.add( selector == null ?
|
|
this.prevObject : this.prevObject.filter(selector)
|
|
);
|
|
}
|
|
});
|
|
|
|
function sibling( cur, dir ) {
|
|
do {
|
|
cur = cur[ dir ];
|
|
} while ( cur && cur.nodeType !== 1 );
|
|
|
|
return cur;
|
|
}
|
|
|
|
jQuery.each({
|
|
parent: function( elem ) {
|
|
var parent = elem.parentNode;
|
|
return parent && parent.nodeType !== 11 ? parent : null;
|
|
},
|
|
parents: function( elem ) {
|
|
return jQuery.dir( elem, "parentNode" );
|
|
},
|
|
parentsUntil: function( elem, i, until ) {
|
|
return jQuery.dir( elem, "parentNode", until );
|
|
},
|
|
next: function( elem ) {
|
|
return sibling( elem, "nextSibling" );
|
|
},
|
|
prev: function( elem ) {
|
|
return sibling( elem, "previousSibling" );
|
|
},
|
|
nextAll: function( elem ) {
|
|
return jQuery.dir( elem, "nextSibling" );
|
|
},
|
|
prevAll: function( elem ) {
|
|
return jQuery.dir( elem, "previousSibling" );
|
|
},
|
|
nextUntil: function( elem, i, until ) {
|
|
return jQuery.dir( elem, "nextSibling", until );
|
|
},
|
|
prevUntil: function( elem, i, until ) {
|
|
return jQuery.dir( elem, "previousSibling", until );
|
|
},
|
|
siblings: function( elem ) {
|
|
return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
|
|
},
|
|
children: function( elem ) {
|
|
return jQuery.sibling( elem.firstChild );
|
|
},
|
|
contents: function( elem ) {
|
|
return jQuery.nodeName( elem, "iframe" ) ?
|
|
elem.contentDocument || elem.contentWindow.document :
|
|
jQuery.merge( [], elem.childNodes );
|
|
}
|
|
}, function( name, fn ) {
|
|
jQuery.fn[ name ] = function( until, selector ) {
|
|
var ret = jQuery.map( this, fn, until );
|
|
|
|
if ( name.slice( -5 ) !== "Until" ) {
|
|
selector = until;
|
|
}
|
|
|
|
if ( selector && typeof selector === "string" ) {
|
|
ret = jQuery.filter( selector, ret );
|
|
}
|
|
|
|
if ( this.length > 1 ) {
|
|
// Remove duplicates
|
|
if ( !guaranteedUnique[ name ] ) {
|
|
ret = jQuery.unique( ret );
|
|
}
|
|
|
|
// Reverse order for parents* and prev-derivatives
|
|
if ( rparentsprev.test( name ) ) {
|
|
ret = ret.reverse();
|
|
}
|
|
}
|
|
|
|
return this.pushStack( ret );
|
|
};
|
|
});
|
|
|
|
jQuery.extend({
|
|
filter: function( expr, elems, not ) {
|
|
var elem = elems[ 0 ];
|
|
|
|
if ( not ) {
|
|
expr = ":not(" + expr + ")";
|
|
}
|
|
|
|
return elems.length === 1 && elem.nodeType === 1 ?
|
|
jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
|
|
jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
|
|
return elem.nodeType === 1;
|
|
}));
|
|
},
|
|
|
|
dir: function( elem, dir, until ) {
|
|
var matched = [],
|
|
cur = elem[ dir ];
|
|
|
|
while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
|
|
if ( cur.nodeType === 1 ) {
|
|
matched.push( cur );
|
|
}
|
|
cur = cur[dir];
|
|
}
|
|
return matched;
|
|
},
|
|
|
|
sibling: function( n, elem ) {
|
|
var r = [];
|
|
|
|
for ( ; n; n = n.nextSibling ) {
|
|
if ( n.nodeType === 1 && n !== elem ) {
|
|
r.push( n );
|
|
}
|
|
}
|
|
|
|
return r;
|
|
}
|
|
});
|
|
|
|
// Implement the identical functionality for filter and not
|
|
function winnow( elements, qualifier, not ) {
|
|
if ( jQuery.isFunction( qualifier ) ) {
|
|
return jQuery.grep( elements, function( elem, i ) {
|
|
/* jshint -W018 */
|
|
return !!qualifier.call( elem, i, elem ) !== not;
|
|
});
|
|
|
|
}
|
|
|
|
if ( qualifier.nodeType ) {
|
|
return jQuery.grep( elements, function( elem ) {
|
|
return ( elem === qualifier ) !== not;
|
|
});
|
|
|
|
}
|
|
|
|
if ( typeof qualifier === "string" ) {
|
|
if ( isSimple.test( qualifier ) ) {
|
|
return jQuery.filter( qualifier, elements, not );
|
|
}
|
|
|
|
qualifier = jQuery.filter( qualifier, elements );
|
|
}
|
|
|
|
return jQuery.grep( elements, function( elem ) {
|
|
return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not;
|
|
});
|
|
}
|
|
function createSafeFragment( document ) {
|
|
var list = nodeNames.split( "|" ),
|
|
safeFrag = document.createDocumentFragment();
|
|
|
|
if ( safeFrag.createElement ) {
|
|
while ( list.length ) {
|
|
safeFrag.createElement(
|
|
list.pop()
|
|
);
|
|
}
|
|
}
|
|
return safeFrag;
|
|
}
|
|
|
|
var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
|
|
"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
|
|
rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
|
|
rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
|
|
rleadingWhitespace = /^\s+/,
|
|
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
|
|
rtagName = /<([\w:]+)/,
|
|
rtbody = /<tbody/i,
|
|
rhtml = /<|&#?\w+;/,
|
|
rnoInnerhtml = /<(?:script|style|link)/i,
|
|
manipulation_rcheckableType = /^(?:checkbox|radio)$/i,
|
|
// checked="checked" or checked
|
|
rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
|
|
rscriptType = /^$|\/(?:java|ecma)script/i,
|
|
rscriptTypeMasked = /^true\/(.*)/,
|
|
rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
|
|
|
|
// We have to close these tags to support XHTML (#13200)
|
|
wrapMap = {
|
|
option: [ 1, "<select multiple='multiple'>", "</select>" ],
|
|
legend: [ 1, "<fieldset>", "</fieldset>" ],
|
|
area: [ 1, "<map>", "</map>" ],
|
|
param: [ 1, "<object>", "</object>" ],
|
|
thead: [ 1, "<table>", "</table>" ],
|
|
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
|
|
col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
|
|
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
|
|
|
|
// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
|
|
// unless wrapped in a div with non-breaking characters in front of it.
|
|
_default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ]
|
|
},
|
|
safeFragment = createSafeFragment( document ),
|
|
fragmentDiv = safeFragment.appendChild( document.createElement("div") );
|
|
|
|
wrapMap.optgroup = wrapMap.option;
|
|
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
|
|
wrapMap.th = wrapMap.td;
|
|
|
|
jQuery.fn.extend({
|
|
text: function( value ) {
|
|
return jQuery.access( this, function( value ) {
|
|
return value === undefined ?
|
|
jQuery.text( this ) :
|
|
this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
|
|
}, null, value, arguments.length );
|
|
},
|
|
|
|
append: function() {
|
|
return this.domManip( arguments, function( elem ) {
|
|
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
|
|
var target = manipulationTarget( this, elem );
|
|
target.appendChild( elem );
|
|
}
|
|
});
|
|
},
|
|
|
|
prepend: function() {
|
|
return this.domManip( arguments, function( elem ) {
|
|
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
|
|
var target = manipulationTarget( this, elem );
|
|
target.insertBefore( elem, target.firstChild );
|
|
}
|
|
});
|
|
},
|
|
|
|
before: function() {
|
|
return this.domManip( arguments, function( elem ) {
|
|
if ( this.parentNode ) {
|
|
this.parentNode.insertBefore( elem, this );
|
|
}
|
|
});
|
|
},
|
|
|
|
after: function() {
|
|
return this.domManip( arguments, function( elem ) {
|
|
if ( this.parentNode ) {
|
|
this.parentNode.insertBefore( elem, this.nextSibling );
|
|
}
|
|
});
|
|
},
|
|
|
|
// keepData is for internal use only--do not document
|
|
remove: function( selector, keepData ) {
|
|
var elem,
|
|
elems = selector ? jQuery.filter( selector, this ) : this,
|
|
i = 0;
|
|
|
|
for ( ; (elem = elems[i]) != null; i++ ) {
|
|
|
|
if ( !keepData && elem.nodeType === 1 ) {
|
|
jQuery.cleanData( getAll( elem ) );
|
|
}
|
|
|
|
if ( elem.parentNode ) {
|
|
if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
|
|
setGlobalEval( getAll( elem, "script" ) );
|
|
}
|
|
elem.parentNode.removeChild( elem );
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
empty: function() {
|
|
var elem,
|
|
i = 0;
|
|
|
|
for ( ; (elem = this[i]) != null; i++ ) {
|
|
// Remove element nodes and prevent memory leaks
|
|
if ( elem.nodeType === 1 ) {
|
|
jQuery.cleanData( getAll( elem, false ) );
|
|
}
|
|
|
|
// Remove any remaining nodes
|
|
while ( elem.firstChild ) {
|
|
elem.removeChild( elem.firstChild );
|
|
}
|
|
|
|
// If this is a select, ensure that it displays empty (#12336)
|
|
// Support: IE<9
|
|
if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
|
|
elem.options.length = 0;
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
clone: function( dataAndEvents, deepDataAndEvents ) {
|
|
dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
|
|
deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
|
|
|
|
return this.map( function () {
|
|
return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
|
|
});
|
|
},
|
|
|
|
html: function( value ) {
|
|
return jQuery.access( this, function( value ) {
|
|
var elem = this[0] || {},
|
|
i = 0,
|
|
l = this.length;
|
|
|
|
if ( value === undefined ) {
|
|
return elem.nodeType === 1 ?
|
|
elem.innerHTML.replace( rinlinejQuery, "" ) :
|
|
undefined;
|
|
}
|
|
|
|
// See if we can take a shortcut and just use innerHTML
|
|
if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
|
|
( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) &&
|
|
( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
|
|
!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
|
|
|
|
value = value.replace( rxhtmlTag, "<$1></$2>" );
|
|
|
|
try {
|
|
for (; i < l; i++ ) {
|
|
// Remove element nodes and prevent memory leaks
|
|
elem = this[i] || {};
|
|
if ( elem.nodeType === 1 ) {
|
|
jQuery.cleanData( getAll( elem, false ) );
|
|
elem.innerHTML = value;
|
|
}
|
|
}
|
|
|
|
elem = 0;
|
|
|
|
// If using innerHTML throws an exception, use the fallback method
|
|
} catch(e) {}
|
|
}
|
|
|
|
if ( elem ) {
|
|
this.empty().append( value );
|
|
}
|
|
}, null, value, arguments.length );
|
|
},
|
|
|
|
replaceWith: function() {
|
|
var
|
|
// Snapshot the DOM in case .domManip sweeps something relevant into its fragment
|
|
args = jQuery.map( this, function( elem ) {
|
|
return [ elem.nextSibling, elem.parentNode ];
|
|
}),
|
|
i = 0;
|
|
|
|
// Make the changes, replacing each context element with the new content
|
|
this.domManip( arguments, function( elem ) {
|
|
var next = args[ i++ ],
|
|
parent = args[ i++ ];
|
|
|
|
if ( parent ) {
|
|
// Don't use the snapshot next if it has moved (#13810)
|
|
if ( next && next.parentNode !== parent ) {
|
|
next = this.nextSibling;
|
|
}
|
|
jQuery( this ).remove();
|
|
parent.insertBefore( elem, next );
|
|
}
|
|
// Allow new content to include elements from the context set
|
|
}, true );
|
|
|
|
// Force removal if there was no new content (e.g., from empty arguments)
|
|
return i ? this : this.remove();
|
|
},
|
|
|
|
detach: function( selector ) {
|
|
return this.remove( selector, true );
|
|
},
|
|
|
|
domManip: function( args, callback, allowIntersection ) {
|
|
|
|
// Flatten any nested arrays
|
|
args = core_concat.apply( [], args );
|
|
|
|
var first, node, hasScripts,
|
|
scripts, doc, fragment,
|
|
i = 0,
|
|
l = this.length,
|
|
set = this,
|
|
iNoClone = l - 1,
|
|
value = args[0],
|
|
isFunction = jQuery.isFunction( value );
|
|
|
|
// We can't cloneNode fragments that contain checked, in WebKit
|
|
if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {
|
|
return this.each(function( index ) {
|
|
var self = set.eq( index );
|
|
if ( isFunction ) {
|
|
args[0] = value.call( this, index, self.html() );
|
|
}
|
|
self.domManip( args, callback, allowIntersection );
|
|
});
|
|
}
|
|
|
|
if ( l ) {
|
|
fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this );
|
|
first = fragment.firstChild;
|
|
|
|
if ( fragment.childNodes.length === 1 ) {
|
|
fragment = first;
|
|
}
|
|
|
|
if ( first ) {
|
|
scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
|
|
hasScripts = scripts.length;
|
|
|
|
// Use the original fragment for the last item instead of the first because it can end up
|
|
// being emptied incorrectly in certain situations (#8070).
|
|
for ( ; i < l; i++ ) {
|
|
node = fragment;
|
|
|
|
if ( i !== iNoClone ) {
|
|
node = jQuery.clone( node, true, true );
|
|
|
|
// Keep references to cloned scripts for later restoration
|
|
if ( hasScripts ) {
|
|
jQuery.merge( scripts, getAll( node, "script" ) );
|
|
}
|
|
}
|
|
|
|
callback.call( this[i], node, i );
|
|
}
|
|
|
|
if ( hasScripts ) {
|
|
doc = scripts[ scripts.length - 1 ].ownerDocument;
|
|
|
|
// Reenable scripts
|
|
jQuery.map( scripts, restoreScript );
|
|
|
|
// Evaluate executable scripts on first document insertion
|
|
for ( i = 0; i < hasScripts; i++ ) {
|
|
node = scripts[ i ];
|
|
if ( rscriptType.test( node.type || "" ) &&
|
|
!jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
|
|
|
|
if ( node.src ) {
|
|
// Hope ajax is available...
|
|
jQuery._evalUrl( node.src );
|
|
} else {
|
|
jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix #11809: Avoid leaking memory
|
|
fragment = first = null;
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
|
|
// Support: IE<8
|
|
// Manipulating tables requires a tbody
|
|
function manipulationTarget( elem, content ) {
|
|
return jQuery.nodeName( elem, "table" ) &&
|
|
jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ?
|
|
|
|
elem.getElementsByTagName("tbody")[0] ||
|
|
elem.appendChild( elem.ownerDocument.createElement("tbody") ) :
|
|
elem;
|
|
}
|
|
|
|
// Replace/restore the type attribute of script elements for safe DOM manipulation
|
|
function disableScript( elem ) {
|
|
elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type;
|
|
return elem;
|
|
}
|
|
function restoreScript( elem ) {
|
|
var match = rscriptTypeMasked.exec( elem.type );
|
|
if ( match ) {
|
|
elem.type = match[1];
|
|
} else {
|
|
elem.removeAttribute("type");
|
|
}
|
|
return elem;
|
|
}
|
|
|
|
// Mark scripts as having already been evaluated
|
|
function setGlobalEval( elems, refElements ) {
|
|
var elem,
|
|
i = 0;
|
|
for ( ; (elem = elems[i]) != null; i++ ) {
|
|
jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) );
|
|
}
|
|
}
|
|
|
|
function cloneCopyEvent( src, dest ) {
|
|
|
|
if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
|
|
return;
|
|
}
|
|
|
|
var type, i, l,
|
|
oldData = jQuery._data( src ),
|
|
curData = jQuery._data( dest, oldData ),
|
|
events = oldData.events;
|
|
|
|
if ( events ) {
|
|
delete curData.handle;
|
|
curData.events = {};
|
|
|
|
for ( type in events ) {
|
|
for ( i = 0, l = events[ type ].length; i < l; i++ ) {
|
|
jQuery.event.add( dest, type, events[ type ][ i ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
// make the cloned public data object a copy from the original
|
|
if ( curData.data ) {
|
|
curData.data = jQuery.extend( {}, curData.data );
|
|
}
|
|
}
|
|
|
|
function fixCloneNodeIssues( src, dest ) {
|
|
var nodeName, e, data;
|
|
|
|
// We do not need to do anything for non-Elements
|
|
if ( dest.nodeType !== 1 ) {
|
|
return;
|
|
}
|
|
|
|
nodeName = dest.nodeName.toLowerCase();
|
|
|
|
// IE6-8 copies events bound via attachEvent when using cloneNode.
|
|
if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) {
|
|
data = jQuery._data( dest );
|
|
|
|
for ( e in data.events ) {
|
|
jQuery.removeEvent( dest, e, data.handle );
|
|
}
|
|
|
|
// Event data gets referenced instead of copied if the expando gets copied too
|
|
dest.removeAttribute( jQuery.expando );
|
|
}
|
|
|
|
// IE blanks contents when cloning scripts, and tries to evaluate newly-set text
|
|
if ( nodeName === "script" && dest.text !== src.text ) {
|
|
disableScript( dest ).text = src.text;
|
|
restoreScript( dest );
|
|
|
|
// IE6-10 improperly clones children of object elements using classid.
|
|
// IE10 throws NoModificationAllowedError if parent is null, #12132.
|
|
} else if ( nodeName === "object" ) {
|
|
if ( dest.parentNode ) {
|
|
dest.outerHTML = src.outerHTML;
|
|
}
|
|
|
|
// This path appears unavoidable for IE9. When cloning an object
|
|
// element in IE9, the outerHTML strategy above is not sufficient.
|
|
// If the src has innerHTML and the destination does not,
|
|
// copy the src.innerHTML into the dest.innerHTML. #10324
|
|
if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) {
|
|
dest.innerHTML = src.innerHTML;
|
|
}
|
|
|
|
} else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) {
|
|
// IE6-8 fails to persist the checked state of a cloned checkbox
|
|
// or radio button. Worse, IE6-7 fail to give the cloned element
|
|
// a checked appearance if the defaultChecked value isn't also set
|
|
|
|
dest.defaultChecked = dest.checked = src.checked;
|
|
|
|
// IE6-7 get confused and end up setting the value of a cloned
|
|
// checkbox/radio button to an empty string instead of "on"
|
|
if ( dest.value !== src.value ) {
|
|
dest.value = src.value;
|
|
}
|
|
|
|
// IE6-8 fails to return the selected option to the default selected
|
|
// state when cloning options
|
|
} else if ( nodeName === "option" ) {
|
|
dest.defaultSelected = dest.selected = src.defaultSelected;
|
|
|
|
// IE6-8 fails to set the defaultValue to the correct value when
|
|
// cloning other types of input fields
|
|
} else if ( nodeName === "input" || nodeName === "textarea" ) {
|
|
dest.defaultValue = src.defaultValue;
|
|
}
|
|
}
|
|
|
|
jQuery.each({
|
|
appendTo: "append",
|
|
prependTo: "prepend",
|
|
insertBefore: "before",
|
|
insertAfter: "after",
|
|
replaceAll: "replaceWith"
|
|
}, function( name, original ) {
|
|
jQuery.fn[ name ] = function( selector ) {
|
|
var elems,
|
|
i = 0,
|
|
ret = [],
|
|
insert = jQuery( selector ),
|
|
last = insert.length - 1;
|
|
|
|
for ( ; i <= last; i++ ) {
|
|
elems = i === last ? this : this.clone(true);
|
|
jQuery( insert[i] )[ original ]( elems );
|
|
|
|
// Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get()
|
|
core_push.apply( ret, elems.get() );
|
|
}
|
|
|
|
return this.pushStack( ret );
|
|
};
|
|
});
|
|
|
|
function getAll( context, tag ) {
|
|
var elems, elem,
|
|
i = 0,
|
|
found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) :
|
|
typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) :
|
|
undefined;
|
|
|
|
if ( !found ) {
|
|
for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {
|
|
if ( !tag || jQuery.nodeName( elem, tag ) ) {
|
|
found.push( elem );
|
|
} else {
|
|
jQuery.merge( found, getAll( elem, tag ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
|
|
jQuery.merge( [ context ], found ) :
|
|
found;
|
|
}
|
|
|
|
// Used in buildFragment, fixes the defaultChecked property
|
|
function fixDefaultChecked( elem ) {
|
|
if ( manipulation_rcheckableType.test( elem.type ) ) {
|
|
elem.defaultChecked = elem.checked;
|
|
}
|
|
}
|
|
|
|
jQuery.extend({
|
|
clone: function( elem, dataAndEvents, deepDataAndEvents ) {
|
|
var destElements, node, clone, i, srcElements,
|
|
inPage = jQuery.contains( elem.ownerDocument, elem );
|
|
|
|
if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
|
|
clone = elem.cloneNode( true );
|
|
|
|
// IE<=8 does not properly clone detached, unknown element nodes
|
|
} else {
|
|
fragmentDiv.innerHTML = elem.outerHTML;
|
|
fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
|
|
}
|
|
|
|
if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
|
|
(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
|
|
|
|
// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
|
|
destElements = getAll( clone );
|
|
srcElements = getAll( elem );
|
|
|
|
// Fix all IE cloning issues
|
|
for ( i = 0; (node = srcElements[i]) != null; ++i ) {
|
|
// Ensure that the destination node is not null; Fixes #9587
|
|
if ( destElements[i] ) {
|
|
fixCloneNodeIssues( node, destElements[i] );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy the events from the original to the clone
|
|
if ( dataAndEvents ) {
|
|
if ( deepDataAndEvents ) {
|
|
srcElements = srcElements || getAll( elem );
|
|
destElements = destElements || getAll( clone );
|
|
|
|
for ( i = 0; (node = srcElements[i]) != null; i++ ) {
|
|
cloneCopyEvent( node, destElements[i] );
|
|
}
|
|
} else {
|
|
cloneCopyEvent( elem, clone );
|
|
}
|
|
}
|
|
|
|
// Preserve script evaluation history
|
|
destElements = getAll( clone, "script" );
|
|
if ( destElements.length > 0 ) {
|
|
setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
|
|
}
|
|
|
|
destElements = srcElements = node = null;
|
|
|
|
// Return the cloned set
|
|
return clone;
|
|
},
|
|
|
|
buildFragment: function( elems, context, scripts, selection ) {
|
|
var j, elem, contains,
|
|
tmp, tag, tbody, wrap,
|
|
l = elems.length,
|
|
|
|
// Ensure a safe fragment
|
|
safe = createSafeFragment( context ),
|
|
|
|
nodes = [],
|
|
i = 0;
|
|
|
|
for ( ; i < l; i++ ) {
|
|
elem = elems[ i ];
|
|
|
|
if ( elem || elem === 0 ) {
|
|
|
|
// Add nodes directly
|
|
if ( jQuery.type( elem ) === "object" ) {
|
|
jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
|
|
|
|
// Convert non-html into a text node
|
|
} else if ( !rhtml.test( elem ) ) {
|
|
nodes.push( context.createTextNode( elem ) );
|
|
|
|
// Convert html into DOM nodes
|
|
} else {
|
|
tmp = tmp || safe.appendChild( context.createElement("div") );
|
|
|
|
// Deserialize a standard representation
|
|
tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
|
|
wrap = wrapMap[ tag ] || wrapMap._default;
|
|
|
|
tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2];
|
|
|
|
// Descend through wrappers to the right content
|
|
j = wrap[0];
|
|
while ( j-- ) {
|
|
tmp = tmp.lastChild;
|
|
}
|
|
|
|
// Manually add leading whitespace removed by IE
|
|
if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
|
|
nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) );
|
|
}
|
|
|
|
// Remove IE's autoinserted <tbody> from table fragments
|
|
if ( !jQuery.support.tbody ) {
|
|
|
|
// String was a <table>, *may* have spurious <tbody>
|
|
elem = tag === "table" && !rtbody.test( elem ) ?
|
|
tmp.firstChild :
|
|
|
|
// String was a bare <thead> or <tfoot>
|
|
wrap[1] === "<table>" && !rtbody.test( elem ) ?
|
|
tmp :
|
|
0;
|
|
|
|
j = elem && elem.childNodes.length;
|
|
while ( j-- ) {
|
|
if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) {
|
|
elem.removeChild( tbody );
|
|
}
|
|
}
|
|
}
|
|
|
|
jQuery.merge( nodes, tmp.childNodes );
|
|
|
|
// Fix #12392 for WebKit and IE > 9
|
|
tmp.textContent = "";
|
|
|
|
// Fix #12392 for oldIE
|
|
while ( tmp.firstChild ) {
|
|
tmp.removeChild( tmp.firstChild );
|
|
}
|
|
|
|
// Remember the top-level container for proper cleanup
|
|
tmp = safe.lastChild;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix #11356: Clear elements from fragment
|
|
if ( tmp ) {
|
|
safe.removeChild( tmp );
|
|
}
|
|
|
|
// Reset defaultChecked for any radios and checkboxes
|
|
// about to be appended to the DOM in IE 6/7 (#8060)
|
|
if ( !jQuery.support.appendChecked ) {
|
|
jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked );
|
|
}
|
|
|
|
i = 0;
|
|
while ( (elem = nodes[ i++ ]) ) {
|
|
|
|
// #4087 - If origin and destination elements are the same, and this is
|
|
// that element, do not do anything
|
|
if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
|
|
continue;
|
|
}
|
|
|
|
contains = jQuery.contains( elem.ownerDocument, elem );
|
|
|
|
// Append to fragment
|
|
tmp = getAll( safe.appendChild( elem ), "script" );
|
|
|
|
// Preserve script evaluation history
|
|
if ( contains ) {
|
|
setGlobalEval( tmp );
|
|
}
|
|
|
|
// Capture executables
|
|
if ( scripts ) {
|
|
j = 0;
|
|
while ( (elem = tmp[ j++ ]) ) {
|
|
if ( rscriptType.test( elem.type || "" ) ) {
|
|
scripts.push( elem );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tmp = null;
|
|
|
|
return safe;
|
|
},
|
|
|
|
cleanData: function( elems, /* internal */ acceptData ) {
|
|
var elem, type, id, data,
|
|
i = 0,
|
|
internalKey = jQuery.expando,
|
|
cache = jQuery.cache,
|
|
deleteExpando = jQuery.support.deleteExpando,
|
|
special = jQuery.event.special;
|
|
|
|
for ( ; (elem = elems[i]) != null; i++ ) {
|
|
|
|
if ( acceptData || jQuery.acceptData( elem ) ) {
|
|
|
|
id = elem[ internalKey ];
|
|
data = id && cache[ id ];
|
|
|
|
if ( data ) {
|
|
if ( data.events ) {
|
|
for ( type in data.events ) {
|
|
if ( special[ type ] ) {
|
|
jQuery.event.remove( elem, type );
|
|
|
|
// This is a shortcut to avoid jQuery.event.remove's overhead
|
|
} else {
|
|
jQuery.removeEvent( elem, type, data.handle );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove cache only if it was not already removed by jQuery.event.remove
|
|
if ( cache[ id ] ) {
|
|
|
|
delete cache[ id ];
|
|
|
|
// IE does not allow us to delete expando properties from nodes,
|
|
// nor does it have a removeAttribute function on Document nodes;
|
|
// we must handle all of these cases
|
|
if ( deleteExpando ) {
|
|
delete elem[ internalKey ];
|
|
|
|
} else if ( typeof elem.removeAttribute !== core_strundefined ) {
|
|
elem.removeAttribute( internalKey );
|
|
|
|
} else {
|
|
elem[ internalKey ] = null;
|
|
}
|
|
|
|
core_deletedIds.push( id );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_evalUrl: function( url ) {
|
|
return jQuery.ajax({
|
|
url: url,
|
|
type: "GET",
|
|
dataType: "script",
|
|
async: false,
|
|
global: false,
|
|
"throws": true
|
|
});
|
|
}
|
|
});
|
|
jQuery.fn.extend({
|
|
wrapAll: function( html ) {
|
|
if ( jQuery.isFunction( html ) ) {
|
|
return this.each(function(i) {
|
|
jQuery(this).wrapAll( html.call(this, i) );
|
|
});
|
|
}
|
|
|
|
if ( this[0] ) {
|
|
// The elements to wrap the target around
|
|
var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
|
|
|
|
if ( this[0].parentNode ) {
|
|
wrap.insertBefore( this[0] );
|
|
}
|
|
|
|
wrap.map(function() {
|
|
var elem = this;
|
|
|
|
while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
|
|
elem = elem.firstChild;
|
|
}
|
|
|
|
return elem;
|
|
}).append( this );
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
wrapInner: function( html ) {
|
|
if ( jQuery.isFunction( html ) ) {
|
|
return this.each(function(i) {
|
|
jQuery(this).wrapInner( html.call(this, i) );
|
|
});
|
|
}
|
|
|
|
return this.each(function() {
|
|
var self = jQuery( this ),
|
|
contents = self.contents();
|
|
|
|
if ( contents.length ) {
|
|
contents.wrapAll( html );
|
|
|
|
} else {
|
|
self.append( html );
|
|
}
|
|
});
|
|
},
|
|
|
|
wrap: function( html ) {
|
|
var isFunction = jQuery.isFunction( html );
|
|
|
|
return this.each(function(i) {
|
|
jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
|
|
});
|
|
},
|
|
|
|
unwrap: function() {
|
|
return this.parent().each(function() {
|
|
if ( !jQuery.nodeName( this, "body" ) ) {
|
|
jQuery( this ).replaceWith( this.childNodes );
|
|
}
|
|
}).end();
|
|
}
|
|
});
|
|
var iframe, getStyles, curCSS,
|
|
ralpha = /alpha\([^)]*\)/i,
|
|
ropacity = /opacity\s*=\s*([^)]*)/,
|
|
rposition = /^(top|right|bottom|left)$/,
|
|
// swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
|
|
// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
|
|
rdisplayswap = /^(none|table(?!-c[ea]).+)/,
|
|
rmargin = /^margin/,
|
|
rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
|
|
rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
|
|
rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ),
|
|
elemdisplay = { BODY: "block" },
|
|
|
|
cssShow = { position: "absolute", visibility: "hidden", display: "block" },
|
|
cssNormalTransform = {
|
|
letterSpacing: 0,
|
|
fontWeight: 400
|
|
},
|
|
|
|
cssExpand = [ "Top", "Right", "Bottom", "Left" ],
|
|
cssPrefixes = [ "Webkit", "O", "Moz", "ms" ];
|
|
|
|
// return a css property mapped to a potentially vendor prefixed property
|
|
function vendorPropName( style, name ) {
|
|
|
|
// shortcut for names that are not vendor prefixed
|
|
if ( name in style ) {
|
|
return name;
|
|
}
|
|
|
|
// check for vendor prefixed names
|
|
var capName = name.charAt(0).toUpperCase() + name.slice(1),
|
|
origName = name,
|
|
i = cssPrefixes.length;
|
|
|
|
while ( i-- ) {
|
|
name = cssPrefixes[ i ] + capName;
|
|
if ( name in style ) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
return origName;
|
|
}
|
|
|
|
function isHidden( elem, el ) {
|
|
// isHidden might be called from jQuery#filter function;
|
|
// in that case, element will be second argument
|
|
elem = el || elem;
|
|
return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
|
|
}
|
|
|
|
function showHide( elements, show ) {
|
|
var display, elem, hidden,
|
|
values = [],
|
|
index = 0,
|
|
length = elements.length;
|
|
|
|
for ( ; index < length; index++ ) {
|
|
elem = elements[ index ];
|
|
if ( !elem.style ) {
|
|
continue;
|
|
}
|
|
|
|
values[ index ] = jQuery._data( elem, "olddisplay" );
|
|
display = elem.style.display;
|
|
if ( show ) {
|
|
// Reset the inline display of this element to learn if it is
|
|
// being hidden by cascaded rules or not
|
|
if ( !values[ index ] && display === "none" ) {
|
|
elem.style.display = "";
|
|
}
|
|
|
|
// Set elements which have been overridden with display: none
|
|
// in a stylesheet to whatever the default browser style is
|
|
// for such an element
|
|
if ( elem.style.display === "" && isHidden( elem ) ) {
|
|
values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
|
|
}
|
|
} else {
|
|
|
|
if ( !values[ index ] ) {
|
|
hidden = isHidden( elem );
|
|
|
|
if ( display && display !== "none" || !hidden ) {
|
|
jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the display of most of the elements in a second loop
|
|
// to avoid the constant reflow
|
|
for ( index = 0; index < length; index++ ) {
|
|
elem = elements[ index ];
|
|
if ( !elem.style ) {
|
|
continue;
|
|
}
|
|
if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
|
|
elem.style.display = show ? values[ index ] || "" : "none";
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
jQuery.fn.extend({
|
|
css: function( name, value ) {
|
|
return jQuery.access( this, function( elem, name, value ) {
|
|
var len, styles,
|
|
map = {},
|
|
i = 0;
|
|
|
|
if ( jQuery.isArray( name ) ) {
|
|
styles = getStyles( elem );
|
|
len = name.length;
|
|
|
|
for ( ; i < len; i++ ) {
|
|
map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
return value !== undefined ?
|
|
jQuery.style( elem, name, value ) :
|
|
jQuery.css( elem, name );
|
|
}, name, value, arguments.length > 1 );
|
|
},
|
|
show: function() {
|
|
return showHide( this, true );
|
|
},
|
|
hide: function() {
|
|
return showHide( this );
|
|
},
|
|
toggle: function( state ) {
|
|
if ( typeof state === "boolean" ) {
|
|
return state ? this.show() : this.hide();
|
|
}
|
|
|
|
return this.each(function() {
|
|
if ( isHidden( this ) ) {
|
|
jQuery( this ).show();
|
|
} else {
|
|
jQuery( this ).hide();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
jQuery.extend({
|
|
// Add in style property hooks for overriding the default
|
|
// behavior of getting and setting a style property
|
|
cssHooks: {
|
|
opacity: {
|
|
get: function( elem, computed ) {
|
|
if ( computed ) {
|
|
// We should always get a number back from opacity
|
|
var ret = curCSS( elem, "opacity" );
|
|
return ret === "" ? "1" : ret;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Don't automatically add "px" to these possibly-unitless properties
|
|
cssNumber: {
|
|
"columnCount": true,
|
|
"fillOpacity": true,
|
|
"fontWeight": true,
|
|
"lineHeight": true,
|
|
"opacity": true,
|
|
"order": true,
|
|
"orphans": true,
|
|
"widows": true,
|
|
"zIndex": true,
|
|
"zoom": true
|
|
},
|
|
|
|
// Add in properties whose names you wish to fix before
|
|
// setting or getting the value
|
|
cssProps: {
|
|
// normalize float css property
|
|
"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
|
|
},
|
|
|
|
// Get and set the style property on a DOM Node
|
|
style: function( elem, name, value, extra ) {
|
|
// Don't set styles on text and comment nodes
|
|
if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
|
|
return;
|
|
}
|
|
|
|
// Make sure that we're working with the right name
|
|
var ret, type, hooks,
|
|
origName = jQuery.camelCase( name ),
|
|
style = elem.style;
|
|
|
|
name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
|
|
|
|
// gets hook for the prefixed version
|
|
// followed by the unprefixed version
|
|
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
|
|
|
|
// Check if we're setting a value
|
|
if ( value !== undefined ) {
|
|
type = typeof value;
|
|
|
|
// convert relative number strings (+= or -=) to relative numbers. #7345
|
|
if ( type === "string" && (ret = rrelNum.exec( value )) ) {
|
|
value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
|
|
// Fixes bug #9237
|
|
type = "number";
|
|
}
|
|
|
|
// Make sure that NaN and null values aren't set. See: #7116
|
|
if ( value == null || type === "number" && isNaN( value ) ) {
|
|
return;
|
|
}
|
|
|
|
// If a number was passed in, add 'px' to the (except for certain CSS properties)
|
|
if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
|
|
value += "px";
|
|
}
|
|
|
|
// Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
|
|
// but it would mean to define eight (for every problematic property) identical functions
|
|
if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) {
|
|
style[ name ] = "inherit";
|
|
}
|
|
|
|
// If a hook was provided, use that value, otherwise just set the specified value
|
|
if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
|
|
|
|
// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
|
|
// Fixes bug #5509
|
|
try {
|
|
style[ name ] = value;
|
|
} catch(e) {}
|
|
}
|
|
|
|
} else {
|
|
// If a hook was provided get the non-computed value from there
|
|
if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
|
|
return ret;
|
|
}
|
|
|
|
// Otherwise just get the value from the style object
|
|
return style[ name ];
|
|
}
|
|
},
|
|
|
|
css: function( elem, name, extra, styles ) {
|
|
var num, val, hooks,
|
|
origName = jQuery.camelCase( name );
|
|
|
|
// Make sure that we're working with the right name
|
|
name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
|
|
|
|
// gets hook for the prefixed version
|
|
// followed by the unprefixed version
|
|
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
|
|
|
|
// If a hook was provided get the computed value from there
|
|
if ( hooks && "get" in hooks ) {
|
|
val = hooks.get( elem, true, extra );
|
|
}
|
|
|
|
// Otherwise, if a way to get the computed value exists, use that
|
|
if ( val === undefined ) {
|
|
val = curCSS( elem, name, styles );
|
|
}
|
|
|
|
//convert "normal" to computed value
|
|
if ( val === "normal" && name in cssNormalTransform ) {
|
|
val = cssNormalTransform[ name ];
|
|
}
|
|
|
|
// Return, converting to number if forced or a qualifier was provided and val looks numeric
|
|
if ( extra === "" || extra ) {
|
|
num = parseFloat( val );
|
|
return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
|
|
}
|
|
return val;
|
|
}
|
|
});
|
|
|
|
// NOTE: we've included the "window" in window.getComputedStyle
|
|
// because jsdom on node.js will break without it.
|
|
if ( window.getComputedStyle ) {
|
|
getStyles = function( elem ) {
|
|
return window.getComputedStyle( elem, null );
|
|
};
|
|
|
|
curCSS = function( elem, name, _computed ) {
|
|
var width, minWidth, maxWidth,
|
|
computed = _computed || getStyles( elem ),
|
|
|
|
// getPropertyValue is only needed for .css('filter') in IE9, see #12537
|
|
ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined,
|
|
style = elem.style;
|
|
|
|
if ( computed ) {
|
|
|
|
if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
|
|
ret = jQuery.style( elem, name );
|
|
}
|
|
|
|
// A tribute to the "awesome hack by Dean Edwards"
|
|
// Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
|
|
// Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
|
|
// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
|
|
if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
|
|
|
|
// Remember the original values
|
|
width = style.width;
|
|
minWidth = style.minWidth;
|
|
maxWidth = style.maxWidth;
|
|
|
|
// Put in the new values to get a computed value out
|
|
style.minWidth = style.maxWidth = style.width = ret;
|
|
ret = computed.width;
|
|
|
|
// Revert the changed values
|
|
style.width = width;
|
|
style.minWidth = minWidth;
|
|
style.maxWidth = maxWidth;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
} else if ( document.documentElement.currentStyle ) {
|
|
getStyles = function( elem ) {
|
|
return elem.currentStyle;
|
|
};
|
|
|
|
curCSS = function( elem, name, _computed ) {
|
|
var left, rs, rsLeft,
|
|
computed = _computed || getStyles( elem ),
|
|
ret = computed ? computed[ name ] : undefined,
|
|
style = elem.style;
|
|
|
|
// Avoid setting ret to empty string here
|
|
// so we don't default to auto
|
|
if ( ret == null && style && style[ name ] ) {
|
|
ret = style[ name ];
|
|
}
|
|
|
|
// From the awesome hack by Dean Edwards
|
|
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
|
|
|
|
// If we're not dealing with a regular pixel number
|
|
// but a number that has a weird ending, we need to convert it to pixels
|
|
// but not position css attributes, as those are proportional to the parent element instead
|
|
// and we can't measure the parent instead because it might trigger a "stacking dolls" problem
|
|
if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
|
|
|
|
// Remember the original values
|
|
left = style.left;
|
|
rs = elem.runtimeStyle;
|
|
rsLeft = rs && rs.left;
|
|
|
|
// Put in the new values to get a computed value out
|
|
if ( rsLeft ) {
|
|
rs.left = elem.currentStyle.left;
|
|
}
|
|
style.left = name === "fontSize" ? "1em" : ret;
|
|
ret = style.pixelLeft + "px";
|
|
|
|
// Revert the changed values
|
|
style.left = left;
|
|
if ( rsLeft ) {
|
|
rs.left = rsLeft;
|
|
}
|
|
}
|
|
|
|
return ret === "" ? "auto" : ret;
|
|
};
|
|
}
|
|
|
|
function setPositiveNumber( elem, value, subtract ) {
|
|
var matches = rnumsplit.exec( value );
|
|
return matches ?
|
|
// Guard against undefined "subtract", e.g., when used as in cssHooks
|
|
Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
|
|
value;
|
|
}
|
|
|
|
function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
|
|
var i = extra === ( isBorderBox ? "border" : "content" ) ?
|
|
// If we already have the right measurement, avoid augmentation
|
|
4 :
|
|
// Otherwise initialize for horizontal or vertical properties
|
|
name === "width" ? 1 : 0,
|
|
|
|
val = 0;
|
|
|
|
for ( ; i < 4; i += 2 ) {
|
|
// both box models exclude margin, so add it if we want it
|
|
if ( extra === "margin" ) {
|
|
val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
|
|
}
|
|
|
|
if ( isBorderBox ) {
|
|
// border-box includes padding, so remove it if we want content
|
|
if ( extra === "content" ) {
|
|
val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
|
|
}
|
|
|
|
// at this point, extra isn't border nor margin, so remove border
|
|
if ( extra !== "margin" ) {
|
|
val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
|
|
}
|
|
} else {
|
|
// at this point, extra isn't content, so add padding
|
|
val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
|
|
|
|
// at this point, extra isn't content nor padding, so add border
|
|
if ( extra !== "padding" ) {
|
|
val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
|
|
}
|
|
}
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
function getWidthOrHeight( elem, name, extra ) {
|
|
|
|
// Start with offset property, which is equivalent to the border-box value
|
|
var valueIsBorderBox = true,
|
|
val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
|
|
styles = getStyles( elem ),
|
|
isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
|
|
|
|
// some non-html elements return undefined for offsetWidth, so check for null/undefined
|
|
// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
|
|
// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
|
|
if ( val <= 0 || val == null ) {
|
|
// Fall back to computed then uncomputed css if necessary
|
|
val = curCSS( elem, name, styles );
|
|
if ( val < 0 || val == null ) {
|
|
val = elem.style[ name ];
|
|
}
|
|
|
|
// Computed unit is not pixels. Stop here and return.
|
|
if ( rnumnonpx.test(val) ) {
|
|
return val;
|
|
}
|
|
|
|
// we need the check for style in case a browser which returns unreliable values
|
|
// for getComputedStyle silently falls back to the reliable elem.style
|
|
valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
|
|
|
|
// Normalize "", auto, and prepare for extra
|
|
val = parseFloat( val ) || 0;
|
|
}
|
|
|
|
// use the active box-sizing model to add/subtract irrelevant styles
|
|
return ( val +
|
|
augmentWidthOrHeight(
|
|
elem,
|
|
name,
|
|
extra || ( isBorderBox ? "border" : "content" ),
|
|
valueIsBorderBox,
|
|
styles
|
|
)
|
|
) + "px";
|
|
}
|
|
|
|
// Try to determine the default display value of an element
|
|
function css_defaultDisplay( nodeName ) {
|
|
var doc = document,
|
|
display = elemdisplay[ nodeName ];
|
|
|
|
if ( !display ) {
|
|
display = actualDisplay( nodeName, doc );
|
|
|
|
// If the simple way fails, read from inside an iframe
|
|
if ( display === "none" || !display ) {
|
|
// Use the already-created iframe if possible
|
|
iframe = ( iframe ||
|
|
jQuery("<iframe frameborder='0' width='0' height='0'/>")
|
|
.css( "cssText", "display:block !important" )
|
|
).appendTo( doc.documentElement );
|
|
|
|
// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
|
|
doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document;
|
|
doc.write("<!doctype html><html><body>");
|
|
doc.close();
|
|
|
|
display = actualDisplay( nodeName, doc );
|
|
iframe.detach();
|
|
}
|
|
|
|
// Store the correct default display
|
|
elemdisplay[ nodeName ] = display;
|
|
}
|
|
|
|
return display;
|
|
}
|
|
|
|
// Called ONLY from within css_defaultDisplay
|
|
function actualDisplay( name, doc ) {
|
|
var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
|
|
display = jQuery.css( elem[0], "display" );
|
|
elem.remove();
|
|
return display;
|
|
}
|
|
|
|
jQuery.each([ "height", "width" ], function( i, name ) {
|
|
jQuery.cssHooks[ name ] = {
|
|
get: function( elem, computed, extra ) {
|
|
if ( computed ) {
|
|
// certain elements can have dimension info if we invisibly show them
|
|
// however, it must have a current display style that would benefit from this
|
|
return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ?
|
|
jQuery.swap( elem, cssShow, function() {
|
|
return getWidthOrHeight( elem, name, extra );
|
|
}) :
|
|
getWidthOrHeight( elem, name, extra );
|
|
}
|
|
},
|
|
|
|
set: function( elem, value, extra ) {
|
|
var styles = extra && getStyles( elem );
|
|
return setPositiveNumber( elem, value, extra ?
|
|
augmentWidthOrHeight(
|
|
elem,
|
|
name,
|
|
extra,
|
|
jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
|
|
styles
|
|
) : 0
|
|
);
|
|
}
|
|
};
|
|
});
|
|
|
|
if ( !jQuery.support.opacity ) {
|
|
jQuery.cssHooks.opacity = {
|
|
get: function( elem, computed ) {
|
|
// IE uses filters for opacity
|
|
return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
|
|
( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
|
|
computed ? "1" : "";
|
|
},
|
|
|
|
set: function( elem, value ) {
|
|
var style = elem.style,
|
|
currentStyle = elem.currentStyle,
|
|
opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
|
|
filter = currentStyle && currentStyle.filter || style.filter || "";
|
|
|
|
// IE has trouble with opacity if it does not have layout
|
|
// Force it by setting the zoom level
|
|
style.zoom = 1;
|
|
|
|
// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
|
|
// if value === "", then remove inline opacity #12685
|
|
if ( ( value >= 1 || value === "" ) &&
|
|
jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
|
|
style.removeAttribute ) {
|
|
|
|
// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
|
|
// if "filter:" is present at all, clearType is disabled, we want to avoid this
|
|
// style.removeAttribute is IE Only, but so apparently is this code path...
|
|
style.removeAttribute( "filter" );
|
|
|
|
// if there is no filter style applied in a css rule or unset inline opacity, we are done
|
|
if ( value === "" || currentStyle && !currentStyle.filter ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// otherwise, set new filter values
|
|
style.filter = ralpha.test( filter ) ?
|
|
filter.replace( ralpha, opacity ) :
|
|
filter + " " + opacity;
|
|
}
|
|
};
|
|
}
|
|
|
|
// These hooks cannot be added until DOM ready because the support test
|
|
// for it is not run until after DOM ready
|
|
jQuery(function() {
|
|
if ( !jQuery.support.reliableMarginRight ) {
|
|
jQuery.cssHooks.marginRight = {
|
|
get: function( elem, computed ) {
|
|
if ( computed ) {
|
|
// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
|
|
// Work around by temporarily setting element display to inline-block
|
|
return jQuery.swap( elem, { "display": "inline-block" },
|
|
curCSS, [ elem, "marginRight" ] );
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
|
|
// getComputedStyle returns percent when specified for top/left/bottom/right
|
|
// rather than make the css module depend on the offset module, we just check for it here
|
|
if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
|
|
jQuery.each( [ "top", "left" ], function( i, prop ) {
|
|
jQuery.cssHooks[ prop ] = {
|
|
get: function( elem, computed ) {
|
|
if ( computed ) {
|
|
computed = curCSS( elem, prop );
|
|
// if curCSS returns percentage, fallback to offset
|
|
return rnumnonpx.test( computed ) ?
|
|
jQuery( elem ).position()[ prop ] + "px" :
|
|
computed;
|
|
}
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
if ( jQuery.expr && jQuery.expr.filters ) {
|
|
jQuery.expr.filters.hidden = function( elem ) {
|
|
// Support: Opera <= 12.12
|
|
// Opera reports offsetWidths and offsetHeights less than zero on some elements
|
|
return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 ||
|
|
(!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none");
|
|
};
|
|
|
|
jQuery.expr.filters.visible = function( elem ) {
|
|
return !jQuery.expr.filters.hidden( elem );
|
|
};
|
|
}
|
|
|
|
// These hooks are used by animate to expand properties
|
|
jQuery.each({
|
|
margin: "",
|
|
padding: "",
|
|
border: "Width"
|
|
}, function( prefix, suffix ) {
|
|
jQuery.cssHooks[ prefix + suffix ] = {
|
|
expand: function( value ) {
|
|
var i = 0,
|
|
expanded = {},
|
|
|
|
// assumes a single number if not a string
|
|
parts = typeof value === "string" ? value.split(" ") : [ value ];
|
|
|
|
for ( ; i < 4; i++ ) {
|
|
expanded[ prefix + cssExpand[ i ] + suffix ] =
|
|
parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
|
|
}
|
|
|
|
return expanded;
|
|
}
|
|
};
|
|
|
|
if ( !rmargin.test( prefix ) ) {
|
|
jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
|
|
}
|
|
});
|
|
var r20 = /%20/g,
|
|
rbracket = /\[\]$/,
|
|
rCRLF = /\r?\n/g,
|
|
rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
|
|
rsubmittable = /^(?:input|select|textarea|keygen)/i;
|
|
|
|
jQuery.fn.extend({
|
|
serialize: function() {
|
|
return jQuery.param( this.serializeArray() );
|
|
},
|
|
serializeArray: function() {
|
|
return this.map(function(){
|
|
// Can add propHook for "elements" to filter or add form elements
|
|
var elements = jQuery.prop( this, "elements" );
|
|
return elements ? jQuery.makeArray( elements ) : this;
|
|
})
|
|
.filter(function(){
|
|
var type = this.type;
|
|
// Use .is(":disabled") so that fieldset[disabled] works
|
|
return this.name && !jQuery( this ).is( ":disabled" ) &&
|
|
rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
|
|
( this.checked || !manipulation_rcheckableType.test( type ) );
|
|
})
|
|
.map(function( i, elem ){
|
|
var val = jQuery( this ).val();
|
|
|
|
return val == null ?
|
|
null :
|
|
jQuery.isArray( val ) ?
|
|
jQuery.map( val, function( val ){
|
|
return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
|
|
}) :
|
|
{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
|
|
}).get();
|
|
}
|
|
});
|
|
|
|
//Serialize an array of form elements or a set of
|
|
//key/values into a query string
|
|
jQuery.param = function( a, traditional ) {
|
|
var prefix,
|
|
s = [],
|
|
add = function( key, value ) {
|
|
// If value is a function, invoke it and return its value
|
|
value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
|
|
s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
|
|
};
|
|
|
|
// Set traditional to true for jQuery <= 1.3.2 behavior.
|
|
if ( traditional === undefined ) {
|
|
traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
|
|
}
|
|
|
|
// If an array was passed in, assume that it is an array of form elements.
|
|
if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
|
|
// Serialize the form elements
|
|
jQuery.each( a, function() {
|
|
add( this.name, this.value );
|
|
});
|
|
|
|
} else {
|
|
// If traditional, encode the "old" way (the way 1.3.2 or older
|
|
// did it), otherwise encode params recursively.
|
|
for ( prefix in a ) {
|
|
buildParams( prefix, a[ prefix ], traditional, add );
|
|
}
|
|
}
|
|
|
|
// Return the resulting serialization
|
|
return s.join( "&" ).replace( r20, "+" );
|
|
};
|
|
|
|
function buildParams( prefix, obj, traditional, add ) {
|
|
var name;
|
|
|
|
if ( jQuery.isArray( obj ) ) {
|
|
// Serialize array item.
|
|
jQuery.each( obj, function( i, v ) {
|
|
if ( traditional || rbracket.test( prefix ) ) {
|
|
// Treat each array item as a scalar.
|
|
add( prefix, v );
|
|
|
|
} else {
|
|
// Item is non-scalar (array or object), encode its numeric index.
|
|
buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
|
|
}
|
|
});
|
|
|
|
} else if ( !traditional && jQuery.type( obj ) === "object" ) {
|
|
// Serialize object item.
|
|
for ( name in obj ) {
|
|
buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
|
|
}
|
|
|
|
} else {
|
|
// Serialize scalar item.
|
|
add( prefix, obj );
|
|
}
|
|
}
|
|
jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
|
|
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
|
|
"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
|
|
|
|
// Handle event binding
|
|
jQuery.fn[ name ] = function( data, fn ) {
|
|
return arguments.length > 0 ?
|
|
this.on( name, null, data, fn ) :
|
|
this.trigger( name );
|
|
};
|
|
});
|
|
|
|
jQuery.fn.extend({
|
|
hover: function( fnOver, fnOut ) {
|
|
return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
|
|
},
|
|
|
|
bind: function( types, data, fn ) {
|
|
return this.on( types, null, data, fn );
|
|
},
|
|
unbind: function( types, fn ) {
|
|
return this.off( types, null, fn );
|
|
},
|
|
|
|
delegate: function( selector, types, data, fn ) {
|
|
return this.on( types, selector, data, fn );
|
|
},
|
|
undelegate: function( selector, types, fn ) {
|
|
// ( namespace ) or ( selector, types [, fn] )
|
|
return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
|
|
}
|
|
});
|
|
var
|
|
// Document location
|
|
ajaxLocParts,
|
|
ajaxLocation,
|
|
ajax_nonce = jQuery.now(),
|
|
|
|
ajax_rquery = /\?/,
|
|
rhash = /#.*$/,
|
|
rts = /([?&])_=[^&]*/,
|
|
rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
|
|
// #7653, #8125, #8152: local protocol detection
|
|
rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
|
|
rnoContent = /^(?:GET|HEAD)$/,
|
|
rprotocol = /^\/\//,
|
|
rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
|
|
|
|
// Keep a copy of the old load method
|
|
_load = jQuery.fn.load,
|
|
|
|
/* Prefilters
|
|
* 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
|
|
* 2) These are called:
|
|
* - BEFORE asking for a transport
|
|
* - AFTER param serialization (s.data is a string if s.processData is true)
|
|
* 3) key is the dataType
|
|
* 4) the catchall symbol "*" can be used
|
|
* 5) execution will start with transport dataType and THEN continue down to "*" if needed
|
|
*/
|
|
prefilters = {},
|
|
|
|
/* Transports bindings
|
|
* 1) key is the dataType
|
|
* 2) the catchall symbol "*" can be used
|
|
* 3) selection will start with transport dataType and THEN go to "*" if needed
|
|
*/
|
|
transports = {},
|
|
|
|
// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
|
|
allTypes = "*/".concat("*");
|
|
|
|
// #8138, IE may throw an exception when accessing
|
|
// a field from window.location if document.domain has been set
|
|
try {
|
|
ajaxLocation = location.href;
|
|
} catch( e ) {
|
|
// Use the href attribute of an A element
|
|
// since IE will modify it given document.location
|
|
ajaxLocation = document.createElement( "a" );
|
|
ajaxLocation.href = "";
|
|
ajaxLocation = ajaxLocation.href;
|
|
}
|
|
|
|
// Segment location into parts
|
|
ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
|
|
|
|
// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
|
|
function addToPrefiltersOrTransports( structure ) {
|
|
|
|
// dataTypeExpression is optional and defaults to "*"
|
|
return function( dataTypeExpression, func ) {
|
|
|
|
if ( typeof dataTypeExpression !== "string" ) {
|
|
func = dataTypeExpression;
|
|
dataTypeExpression = "*";
|
|
}
|
|
|
|
var dataType,
|
|
i = 0,
|
|
dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || [];
|
|
|
|
if ( jQuery.isFunction( func ) ) {
|
|
// For each dataType in the dataTypeExpression
|
|
while ( (dataType = dataTypes[i++]) ) {
|
|
// Prepend if requested
|
|
if ( dataType[0] === "+" ) {
|
|
dataType = dataType.slice( 1 ) || "*";
|
|
(structure[ dataType ] = structure[ dataType ] || []).unshift( func );
|
|
|
|
// Otherwise append
|
|
} else {
|
|
(structure[ dataType ] = structure[ dataType ] || []).push( func );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Base inspection function for prefilters and transports
|
|
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
|
|
|
|
var inspected = {},
|
|
seekingTransport = ( structure === transports );
|
|
|
|
function inspect( dataType ) {
|
|
var selected;
|
|
inspected[ dataType ] = true;
|
|
jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
|
|
var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
|
|
if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
|
|
options.dataTypes.unshift( dataTypeOrTransport );
|
|
inspect( dataTypeOrTransport );
|
|
return false;
|
|
} else if ( seekingTransport ) {
|
|
return !( selected = dataTypeOrTransport );
|
|
}
|
|
});
|
|
return selected;
|
|
}
|
|
|
|
return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
|
|
}
|
|
|
|
// A special extend for ajax options
|
|
// that takes "flat" options (not to be deep extended)
|
|
// Fixes #9887
|
|
function ajaxExtend( target, src ) {
|
|
var deep, key,
|
|
flatOptions = jQuery.ajaxSettings.flatOptions || {};
|
|
|
|
for ( key in src ) {
|
|
if ( src[ key ] !== undefined ) {
|
|
( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];
|
|
}
|
|
}
|
|
if ( deep ) {
|
|
jQuery.extend( true, target, deep );
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
jQuery.fn.load = function( url, params, callback ) {
|
|
if ( typeof url !== "string" && _load ) {
|
|
return _load.apply( this, arguments );
|
|
}
|
|
|
|
var selector, response, type,
|
|
self = this,
|
|
off = url.indexOf(" ");
|
|
|
|
if ( off >= 0 ) {
|
|
selector = url.slice( off, url.length );
|
|
url = url.slice( 0, off );
|
|
}
|
|
|
|
// If it's a function
|
|
if ( jQuery.isFunction( params ) ) {
|
|
|
|
// We assume that it's the callback
|
|
callback = params;
|
|
params = undefined;
|
|
|
|
// Otherwise, build a param string
|
|
} else if ( params && typeof params === "object" ) {
|
|
type = "POST";
|
|
}
|
|
|
|
// If we have elements to modify, make the request
|
|
if ( self.length > 0 ) {
|
|
jQuery.ajax({
|
|
url: url,
|
|
|
|
// if "type" variable is undefined, then "GET" method will be used
|
|
type: type,
|
|
dataType: "html",
|
|
data: params
|
|
}).done(function( responseText ) {
|
|
|
|
// Save response for use in complete callback
|
|
response = arguments;
|
|
|
|
self.html( selector ?
|
|
|
|
// If a selector was specified, locate the right elements in a dummy div
|
|
// Exclude scripts to avoid IE 'Permission Denied' errors
|
|
jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) :
|
|
|
|
// Otherwise use the full result
|
|
responseText );
|
|
|
|
}).complete( callback && function( jqXHR, status ) {
|
|
self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
|
|
});
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
// Attach a bunch of functions for handling common AJAX events
|
|
jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){
|
|
jQuery.fn[ type ] = function( fn ){
|
|
return this.on( type, fn );
|
|
};
|
|
});
|
|
|
|
jQuery.extend({
|
|
|
|
// Counter for holding the number of active queries
|
|
active: 0,
|
|
|
|
// Last-Modified header cache for next request
|
|
lastModified: {},
|
|
etag: {},
|
|
|
|
ajaxSettings: {
|
|
url: ajaxLocation,
|
|
type: "GET",
|
|
isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
|
|
global: true,
|
|
processData: true,
|
|
async: true,
|
|
contentType: "application/x-www-form-urlencoded; charset=UTF-8",
|
|
/*
|
|
timeout: 0,
|
|
data: null,
|
|
dataType: null,
|
|
username: null,
|
|
password: null,
|
|
cache: null,
|
|
throws: false,
|
|
traditional: false,
|
|
headers: {},
|
|
*/
|
|
|
|
accepts: {
|
|
"*": allTypes,
|
|
text: "text/plain",
|
|
html: "text/html",
|
|
xml: "application/xml, text/xml",
|
|
json: "application/json, text/javascript"
|
|
},
|
|
|
|
contents: {
|
|
xml: /xml/,
|
|
html: /html/,
|
|
json: /json/
|
|
},
|
|
|
|
responseFields: {
|
|
xml: "responseXML",
|
|
text: "responseText",
|
|
json: "responseJSON"
|
|
},
|
|
|
|
// Data converters
|
|
// Keys separate source (or catchall "*") and destination types with a single space
|
|
converters: {
|
|
|
|
// Convert anything to text
|
|
"* text": String,
|
|
|
|
// Text to html (true = no transformation)
|
|
"text html": true,
|
|
|
|
// Evaluate text as a json expression
|
|
"text json": jQuery.parseJSON,
|
|
|
|
// Parse text as xml
|
|
"text xml": jQuery.parseXML
|
|
},
|
|
|
|
// For options that shouldn't be deep extended:
|
|
// you can add your own custom options here if
|
|
// and when you create one that shouldn't be
|
|
// deep extended (see ajaxExtend)
|
|
flatOptions: {
|
|
url: true,
|
|
context: true
|
|
}
|
|
},
|
|
|
|
// Creates a full fledged settings object into target
|
|
// with both ajaxSettings and settings fields.
|
|
// If target is omitted, writes into ajaxSettings.
|
|
ajaxSetup: function( target, settings ) {
|
|
return settings ?
|
|
|
|
// Building a settings object
|
|
ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
|
|
|
|
// Extending ajaxSettings
|
|
ajaxExtend( jQuery.ajaxSettings, target );
|
|
},
|
|
|
|
ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
|
|
ajaxTransport: addToPrefiltersOrTransports( transports ),
|
|
|
|
// Main method
|
|
ajax: function( url, options ) {
|
|
|
|
// If url is an object, simulate pre-1.5 signature
|
|
if ( typeof url === "object" ) {
|
|
options = url;
|
|
url = undefined;
|
|
}
|
|
|
|
// Force options to be an object
|
|
options = options || {};
|
|
|
|
var // Cross-domain detection vars
|
|
parts,
|
|
// Loop variable
|
|
i,
|
|
// URL without anti-cache param
|
|
cacheURL,
|
|
// Response headers as string
|
|
responseHeadersString,
|
|
// timeout handle
|
|
timeoutTimer,
|
|
|
|
// To know if global events are to be dispatched
|
|
fireGlobals,
|
|
|
|
transport,
|
|
// Response headers
|
|
responseHeaders,
|
|
// Create the final options object
|
|
s = jQuery.ajaxSetup( {}, options ),
|
|
// Callbacks context
|
|
callbackContext = s.context || s,
|
|
// Context for global events is callbackContext if it is a DOM node or jQuery collection
|
|
globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
|
|
jQuery( callbackContext ) :
|
|
jQuery.event,
|
|
// Deferreds
|
|
deferred = jQuery.Deferred(),
|
|
completeDeferred = jQuery.Callbacks("once memory"),
|
|
// Status-dependent callbacks
|
|
statusCode = s.statusCode || {},
|
|
// Headers (they are sent all at once)
|
|
requestHeaders = {},
|
|
requestHeadersNames = {},
|
|
// The jqXHR state
|
|
state = 0,
|
|
// Default abort message
|
|
strAbort = "canceled",
|
|
// Fake xhr
|
|
jqXHR = {
|
|
readyState: 0,
|
|
|
|
// Builds headers hashtable if needed
|
|
getResponseHeader: function( key ) {
|
|
var match;
|
|
if ( state === 2 ) {
|
|
if ( !responseHeaders ) {
|
|
responseHeaders = {};
|
|
while ( (match = rheaders.exec( responseHeadersString )) ) {
|
|
responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
|
|
}
|
|
}
|
|
match = responseHeaders[ key.toLowerCase() ];
|
|
}
|
|
return match == null ? null : match;
|
|
},
|
|
|
|
// Raw string
|
|
getAllResponseHeaders: function() {
|
|
return state === 2 ? responseHeadersString : null;
|
|
},
|
|
|
|
// Caches the header
|
|
setRequestHeader: function( name, value ) {
|
|
var lname = name.toLowerCase();
|
|
if ( !state ) {
|
|
name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
|
|
requestHeaders[ name ] = value;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// Overrides response content-type header
|
|
overrideMimeType: function( type ) {
|
|
if ( !state ) {
|
|
s.mimeType = type;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// Status-dependent callbacks
|
|
statusCode: function( map ) {
|
|
var code;
|
|
if ( map ) {
|
|
if ( state < 2 ) {
|
|
for ( code in map ) {
|
|
// Lazy-add the new callback in a way that preserves old ones
|
|
statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
|
|
}
|
|
} else {
|
|
// Execute the appropriate callbacks
|
|
jqXHR.always( map[ jqXHR.status ] );
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// Cancel the request
|
|
abort: function( statusText ) {
|
|
var finalText = statusText || strAbort;
|
|
if ( transport ) {
|
|
transport.abort( finalText );
|
|
}
|
|
done( 0, finalText );
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// Attach deferreds
|
|
deferred.promise( jqXHR ).complete = completeDeferred.add;
|
|
jqXHR.success = jqXHR.done;
|
|
jqXHR.error = jqXHR.fail;
|
|
|
|
// Remove hash character (#7531: and string promotion)
|
|
// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
|
|
// Handle falsy url in the settings object (#10093: consistency with old signature)
|
|
// We also use the url parameter if available
|
|
s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
|
|
|
|
// Alias method option to type as per ticket #12004
|
|
s.type = options.method || options.type || s.method || s.type;
|
|
|
|
// Extract dataTypes list
|
|
s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""];
|
|
|
|
// A cross-domain request is in order when we have a protocol:host:port mismatch
|
|
if ( s.crossDomain == null ) {
|
|
parts = rurl.exec( s.url.toLowerCase() );
|
|
s.crossDomain = !!( parts &&
|
|
( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
|
|
( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !==
|
|
( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) )
|
|
);
|
|
}
|
|
|
|
// Convert data if not already a string
|
|
if ( s.data && s.processData && typeof s.data !== "string" ) {
|
|
s.data = jQuery.param( s.data, s.traditional );
|
|
}
|
|
|
|
// Apply prefilters
|
|
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
|
|
|
|
// If request was aborted inside a prefilter, stop there
|
|
if ( state === 2 ) {
|
|
return jqXHR;
|
|
}
|
|
|
|
// We can fire global events as of now if asked to
|
|
fireGlobals = s.global;
|
|
|
|
// Watch for a new set of requests
|
|
if ( fireGlobals && jQuery.active++ === 0 ) {
|
|
jQuery.event.trigger("ajaxStart");
|
|
}
|
|
|
|
// Uppercase the type
|
|
s.type = s.type.toUpperCase();
|
|
|
|
// Determine if request has content
|
|
s.hasContent = !rnoContent.test( s.type );
|
|
|
|
// Save the URL in case we're toying with the If-Modified-Since
|
|
// and/or If-None-Match header later on
|
|
cacheURL = s.url;
|
|
|
|
// More options handling for requests with no content
|
|
if ( !s.hasContent ) {
|
|
|
|
// If data is available, append data to url
|
|
if ( s.data ) {
|
|
cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
|
|
// #9682: remove data so that it's not used in an eventual retry
|
|
delete s.data;
|
|
}
|
|
|
|
// Add anti-cache in url if needed
|
|
if ( s.cache === false ) {
|
|
s.url = rts.test( cacheURL ) ?
|
|
|
|
// If there is already a '_' parameter, set its value
|
|
cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :
|
|
|
|
// Otherwise add one to the end
|
|
cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
|
|
}
|
|
}
|
|
|
|
// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
|
|
if ( s.ifModified ) {
|
|
if ( jQuery.lastModified[ cacheURL ] ) {
|
|
jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
|
|
}
|
|
if ( jQuery.etag[ cacheURL ] ) {
|
|
jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
|
|
}
|
|
}
|
|
|
|
// Set the correct header, if data is being sent
|
|
if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
|
|
jqXHR.setRequestHeader( "Content-Type", s.contentType );
|
|
}
|
|
|
|
// Set the Accepts header for the server, depending on the dataType
|
|
jqXHR.setRequestHeader(
|
|
"Accept",
|
|
s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
|
|
s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
|
|
s.accepts[ "*" ]
|
|
);
|
|
|
|
// Check for headers option
|
|
for ( i in s.headers ) {
|
|
jqXHR.setRequestHeader( i, s.headers[ i ] );
|
|
}
|
|
|
|
// Allow custom headers/mimetypes and early abort
|
|
if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
|
|
// Abort if not done already and return
|
|
return jqXHR.abort();
|
|
}
|
|
|
|
// aborting is no longer a cancellation
|
|
strAbort = "abort";
|
|
|
|
// Install callbacks on deferreds
|
|
for ( i in { success: 1, error: 1, complete: 1 } ) {
|
|
jqXHR[ i ]( s[ i ] );
|
|
}
|
|
|
|
// Get transport
|
|
transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
|
|
|
|
// If no transport, we auto-abort
|
|
if ( !transport ) {
|
|
done( -1, "No Transport" );
|
|
} else {
|
|
jqXHR.readyState = 1;
|
|
|
|
// Send global event
|
|
if ( fireGlobals ) {
|
|
globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
|
|
}
|
|
// Timeout
|
|
if ( s.async && s.timeout > 0 ) {
|
|
timeoutTimer = setTimeout(function() {
|
|
jqXHR.abort("timeout");
|
|
}, s.timeout );
|
|
}
|
|
|
|
try {
|
|
state = 1;
|
|
transport.send( requestHeaders, done );
|
|
} catch ( e ) {
|
|
// Propagate exception as error if not done
|
|
if ( state < 2 ) {
|
|
done( -1, e );
|
|
// Simply rethrow otherwise
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Callback for when everything is done
|
|
function done( status, nativeStatusText, responses, headers ) {
|
|
var isSuccess, success, error, response, modified,
|
|
statusText = nativeStatusText;
|
|
|
|
// Called once
|
|
if ( state === 2 ) {
|
|
return;
|
|
}
|
|
|
|
// State is "done" now
|
|
state = 2;
|
|
|
|
// Clear timeout if it exists
|
|
if ( timeoutTimer ) {
|
|
clearTimeout( timeoutTimer );
|
|
}
|
|
|
|
// Dereference transport for early garbage collection
|
|
// (no matter how long the jqXHR object will be used)
|
|
transport = undefined;
|
|
|
|
// Cache response headers
|
|
responseHeadersString = headers || "";
|
|
|
|
// Set readyState
|
|
jqXHR.readyState = status > 0 ? 4 : 0;
|
|
|
|
// Determine if successful
|
|
isSuccess = status >= 200 && status < 300 || status === 304;
|
|
|
|
// Get response data
|
|
if ( responses ) {
|
|
response = ajaxHandleResponses( s, jqXHR, responses );
|
|
}
|
|
|
|
// Convert no matter what (that way responseXXX fields are always set)
|
|
response = ajaxConvert( s, response, jqXHR, isSuccess );
|
|
|
|
// If successful, handle type chaining
|
|
if ( isSuccess ) {
|
|
|
|
// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
|
|
if ( s.ifModified ) {
|
|
modified = jqXHR.getResponseHeader("Last-Modified");
|
|
if ( modified ) {
|
|
jQuery.lastModified[ cacheURL ] = modified;
|
|
}
|
|
modified = jqXHR.getResponseHeader("etag");
|
|
if ( modified ) {
|
|
jQuery.etag[ cacheURL ] = modified;
|
|
}
|
|
}
|
|
|
|
// if no content
|
|
if ( status === 204 || s.type === "HEAD" ) {
|
|
statusText = "nocontent";
|
|
|
|
// if not modified
|
|
} else if ( status === 304 ) {
|
|
statusText = "notmodified";
|
|
|
|
// If we have data, let's convert it
|
|
} else {
|
|
statusText = response.state;
|
|
success = response.data;
|
|
error = response.error;
|
|
isSuccess = !error;
|
|
}
|
|
} else {
|
|
// We extract error from statusText
|
|
// then normalize statusText and status for non-aborts
|
|
error = statusText;
|
|
if ( status || !statusText ) {
|
|
statusText = "error";
|
|
if ( status < 0 ) {
|
|
status = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set data for the fake xhr object
|
|
jqXHR.status = status;
|
|
jqXHR.statusText = ( nativeStatusText || statusText ) + "";
|
|
|
|
// Success/Error
|
|
if ( isSuccess ) {
|
|
deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
|
|
} else {
|
|
deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
|
|
}
|
|
|
|
// Status-dependent callbacks
|
|
jqXHR.statusCode( statusCode );
|
|
statusCode = undefined;
|
|
|
|
if ( fireGlobals ) {
|
|
globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
|
|
[ jqXHR, s, isSuccess ? success : error ] );
|
|
}
|
|
|
|
// Complete
|
|
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
|
|
|
|
if ( fireGlobals ) {
|
|
globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
|
|
// Handle the global AJAX counter
|
|
if ( !( --jQuery.active ) ) {
|
|
jQuery.event.trigger("ajaxStop");
|
|
}
|
|
}
|
|
}
|
|
|
|
return jqXHR;
|
|
},
|
|
|
|
getJSON: function( url, data, callback ) {
|
|
return jQuery.get( url, data, callback, "json" );
|
|
},
|
|
|
|
getScript: function( url, callback ) {
|
|
return jQuery.get( url, undefined, callback, "script" );
|
|
}
|
|
});
|
|
|
|
jQuery.each( [ "get", "post" ], function( i, method ) {
|
|
jQuery[ method ] = function( url, data, callback, type ) {
|
|
// shift arguments if data argument was omitted
|
|
if ( jQuery.isFunction( data ) ) {
|
|
type = type || callback;
|
|
callback = data;
|
|
data = undefined;
|
|
}
|
|
|
|
return jQuery.ajax({
|
|
url: url,
|
|
type: method,
|
|
dataType: type,
|
|
data: data,
|
|
success: callback
|
|
});
|
|
};
|
|
});
|
|
|
|
/* Handles responses to an ajax request:
|
|
* - finds the right dataType (mediates between content-type and expected dataType)
|
|
* - returns the corresponding response
|
|
*/
|
|
function ajaxHandleResponses( s, jqXHR, responses ) {
|
|
var firstDataType, ct, finalDataType, type,
|
|
contents = s.contents,
|
|
dataTypes = s.dataTypes;
|
|
|
|
// Remove auto dataType and get content-type in the process
|
|
while( dataTypes[ 0 ] === "*" ) {
|
|
dataTypes.shift();
|
|
if ( ct === undefined ) {
|
|
ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
|
|
}
|
|
}
|
|
|
|
// Check if we're dealing with a known content-type
|
|
if ( ct ) {
|
|
for ( type in contents ) {
|
|
if ( contents[ type ] && contents[ type ].test( ct ) ) {
|
|
dataTypes.unshift( type );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check to see if we have a response for the expected dataType
|
|
if ( dataTypes[ 0 ] in responses ) {
|
|
finalDataType = dataTypes[ 0 ];
|
|
} else {
|
|
// Try convertible dataTypes
|
|
for ( type in responses ) {
|
|
if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
|
|
finalDataType = type;
|
|
break;
|
|
}
|
|
if ( !firstDataType ) {
|
|
firstDataType = type;
|
|
}
|
|
}
|
|
// Or just use first one
|
|
finalDataType = finalDataType || firstDataType;
|
|
}
|
|
|
|
// If we found a dataType
|
|
// We add the dataType to the list if needed
|
|
// and return the corresponding response
|
|
if ( finalDataType ) {
|
|
if ( finalDataType !== dataTypes[ 0 ] ) {
|
|
dataTypes.unshift( finalDataType );
|
|
}
|
|
return responses[ finalDataType ];
|
|
}
|
|
}
|
|
|
|
/* Chain conversions given the request and the original response
|
|
* Also sets the responseXXX fields on the jqXHR instance
|
|
*/
|
|
function ajaxConvert( s, response, jqXHR, isSuccess ) {
|
|
var conv2, current, conv, tmp, prev,
|
|
converters = {},
|
|
// Work with a copy of dataTypes in case we need to modify it for conversion
|
|
dataTypes = s.dataTypes.slice();
|
|
|
|
// Create converters map with lowercased keys
|
|
if ( dataTypes[ 1 ] ) {
|
|
for ( conv in s.converters ) {
|
|
converters[ conv.toLowerCase() ] = s.converters[ conv ];
|
|
}
|
|
}
|
|
|
|
current = dataTypes.shift();
|
|
|
|
// Convert to each sequential dataType
|
|
while ( current ) {
|
|
|
|
if ( s.responseFields[ current ] ) {
|
|
jqXHR[ s.responseFields[ current ] ] = response;
|
|
}
|
|
|
|
// Apply the dataFilter if provided
|
|
if ( !prev && isSuccess && s.dataFilter ) {
|
|
response = s.dataFilter( response, s.dataType );
|
|
}
|
|
|
|
prev = current;
|
|
current = dataTypes.shift();
|
|
|
|
if ( current ) {
|
|
|
|
// There's only work to do if current dataType is non-auto
|
|
if ( current === "*" ) {
|
|
|
|
current = prev;
|
|
|
|
// Convert response if prev dataType is non-auto and differs from current
|
|
} else if ( prev !== "*" && prev !== current ) {
|
|
|
|
// Seek a direct converter
|
|
conv = converters[ prev + " " + current ] || converters[ "* " + current ];
|
|
|
|
// If none found, seek a pair
|
|
if ( !conv ) {
|
|
for ( conv2 in converters ) {
|
|
|
|
// If conv2 outputs current
|
|
tmp = conv2.split( " " );
|
|
if ( tmp[ 1 ] === current ) {
|
|
|
|
// If prev can be converted to accepted input
|
|
conv = converters[ prev + " " + tmp[ 0 ] ] ||
|
|
converters[ "* " + tmp[ 0 ] ];
|
|
if ( conv ) {
|
|
// Condense equivalence converters
|
|
if ( conv === true ) {
|
|
conv = converters[ conv2 ];
|
|
|
|
// Otherwise, insert the intermediate dataType
|
|
} else if ( converters[ conv2 ] !== true ) {
|
|
current = tmp[ 0 ];
|
|
dataTypes.unshift( tmp[ 1 ] );
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply converter (if not an equivalence)
|
|
if ( conv !== true ) {
|
|
|
|
// Unless errors are allowed to bubble, catch and return them
|
|
if ( conv && s[ "throws" ] ) {
|
|
response = conv( response );
|
|
} else {
|
|
try {
|
|
response = conv( response );
|
|
} catch ( e ) {
|
|
return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { state: "success", data: response };
|
|
}
|
|
// Install script dataType
|
|
jQuery.ajaxSetup({
|
|
accepts: {
|
|
script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
|
|
},
|
|
contents: {
|
|
script: /(?:java|ecma)script/
|
|
},
|
|
converters: {
|
|
"text script": function( text ) {
|
|
jQuery.globalEval( text );
|
|
return text;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle cache's special case and global
|
|
jQuery.ajaxPrefilter( "script", function( s ) {
|
|
if ( s.cache === undefined ) {
|
|
s.cache = false;
|
|
}
|
|
if ( s.crossDomain ) {
|
|
s.type = "GET";
|
|
s.global = false;
|
|
}
|
|
});
|
|
|
|
// Bind script tag hack transport
|
|
jQuery.ajaxTransport( "script", function(s) {
|
|
|
|
// This transport only deals with cross domain requests
|
|
if ( s.crossDomain ) {
|
|
|
|
var script,
|
|
head = document.head || jQuery("head")[0] || document.documentElement;
|
|
|
|
return {
|
|
|
|
send: function( _, callback ) {
|
|
|
|
script = document.createElement("script");
|
|
|
|
script.async = true;
|
|
|
|
if ( s.scriptCharset ) {
|
|
script.charset = s.scriptCharset;
|
|
}
|
|
|
|
script.src = s.url;
|
|
|
|
// Attach handlers for all browsers
|
|
script.onload = script.onreadystatechange = function( _, isAbort ) {
|
|
|
|
if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
|
|
|
|
// Handle memory leak in IE
|
|
script.onload = script.onreadystatechange = null;
|
|
|
|
// Remove the script
|
|
if ( script.parentNode ) {
|
|
script.parentNode.removeChild( script );
|
|
}
|
|
|
|
// Dereference the script
|
|
script = null;
|
|
|
|
// Callback if not abort
|
|
if ( !isAbort ) {
|
|
callback( 200, "success" );
|
|
}
|
|
}
|
|
};
|
|
|
|
// Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
|
|
// Use native DOM manipulation to avoid our domManip AJAX trickery
|
|
head.insertBefore( script, head.firstChild );
|
|
},
|
|
|
|
abort: function() {
|
|
if ( script ) {
|
|
script.onload( undefined, true );
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
var oldCallbacks = [],
|
|
rjsonp = /(=)\?(?=&|$)|\?\?/;
|
|
|
|
// Default jsonp settings
|
|
jQuery.ajaxSetup({
|
|
jsonp: "callback",
|
|
jsonpCallback: function() {
|
|
var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) );
|
|
this[ callback ] = true;
|
|
return callback;
|
|
}
|
|
});
|
|
|
|
// Detect, normalize options and install callbacks for jsonp requests
|
|
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
|
|
|
|
var callbackName, overwritten, responseContainer,
|
|
jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
|
|
"url" :
|
|
typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
|
|
);
|
|
|
|
// Handle iff the expected data type is "jsonp" or we have a parameter to set
|
|
if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
|
|
|
|
// Get callback name, remembering preexisting value associated with it
|
|
callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
|
|
s.jsonpCallback() :
|
|
s.jsonpCallback;
|
|
|
|
// Insert callback into url or form data
|
|
if ( jsonProp ) {
|
|
s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
|
|
} else if ( s.jsonp !== false ) {
|
|
s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
|
|
}
|
|
|
|
// Use data converter to retrieve json after script execution
|
|
s.converters["script json"] = function() {
|
|
if ( !responseContainer ) {
|
|
jQuery.error( callbackName + " was not called" );
|
|
}
|
|
return responseContainer[ 0 ];
|
|
};
|
|
|
|
// force json dataType
|
|
s.dataTypes[ 0 ] = "json";
|
|
|
|
// Install callback
|
|
overwritten = window[ callbackName ];
|
|
window[ callbackName ] = function() {
|
|
responseContainer = arguments;
|
|
};
|
|
|
|
// Clean-up function (fires after converters)
|
|
jqXHR.always(function() {
|
|
// Restore preexisting value
|
|
window[ callbackName ] = overwritten;
|
|
|
|
// Save back as free
|
|
if ( s[ callbackName ] ) {
|
|
// make sure that re-using the options doesn't screw things around
|
|
s.jsonpCallback = originalSettings.jsonpCallback;
|
|
|
|
// save the callback name for future use
|
|
oldCallbacks.push( callbackName );
|
|
}
|
|
|
|
// Call if it was a function and we have a response
|
|
if ( responseContainer && jQuery.isFunction( overwritten ) ) {
|
|
overwritten( responseContainer[ 0 ] );
|
|
}
|
|
|
|
responseContainer = overwritten = undefined;
|
|
});
|
|
|
|
// Delegate to script
|
|
return "script";
|
|
}
|
|
});
|
|
var xhrCallbacks, xhrSupported,
|
|
xhrId = 0,
|
|
// #5280: Internet Explorer will keep connections alive if we don't abort on unload
|
|
xhrOnUnloadAbort = window.ActiveXObject && function() {
|
|
// Abort all pending requests
|
|
var key;
|
|
for ( key in xhrCallbacks ) {
|
|
xhrCallbacks[ key ]( undefined, true );
|
|
}
|
|
};
|
|
|
|
// Functions to create xhrs
|
|
function createStandardXHR() {
|
|
try {
|
|
return new window.XMLHttpRequest();
|
|
} catch( e ) {}
|
|
}
|
|
|
|
function createActiveXHR() {
|
|
try {
|
|
return new window.ActiveXObject("Microsoft.XMLHTTP");
|
|
} catch( e ) {}
|
|
}
|
|
|
|
// Create the request object
|
|
// (This is still attached to ajaxSettings for backward compatibility)
|
|
jQuery.ajaxSettings.xhr = window.ActiveXObject ?
|
|
/* Microsoft failed to properly
|
|
* implement the XMLHttpRequest in IE7 (can't request local files),
|
|
* so we use the ActiveXObject when it is available
|
|
* Additionally XMLHttpRequest can be disabled in IE7/IE8 so
|
|
* we need a fallback.
|
|
*/
|
|
function() {
|
|
return !this.isLocal && createStandardXHR() || createActiveXHR();
|
|
} :
|
|
// For all other browsers, use the standard XMLHttpRequest object
|
|
createStandardXHR;
|
|
|
|
// Determine support properties
|
|
xhrSupported = jQuery.ajaxSettings.xhr();
|
|
jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
|
|
xhrSupported = jQuery.support.ajax = !!xhrSupported;
|
|
|
|
// Create transport if the browser can provide an xhr
|
|
if ( xhrSupported ) {
|
|
|
|
jQuery.ajaxTransport(function( s ) {
|
|
// Cross domain only allowed if supported through XMLHttpRequest
|
|
if ( !s.crossDomain || jQuery.support.cors ) {
|
|
|
|
var callback;
|
|
|
|
return {
|
|
send: function( headers, complete ) {
|
|
|
|
// Get a new xhr
|
|
var handle, i,
|
|
xhr = s.xhr();
|
|
|
|
// Open the socket
|
|
// Passing null username, generates a login popup on Opera (#2865)
|
|
if ( s.username ) {
|
|
xhr.open( s.type, s.url, s.async, s.username, s.password );
|
|
} else {
|
|
xhr.open( s.type, s.url, s.async );
|
|
}
|
|
|
|
// Apply custom fields if provided
|
|
if ( s.xhrFields ) {
|
|
for ( i in s.xhrFields ) {
|
|
xhr[ i ] = s.xhrFields[ i ];
|
|
}
|
|
}
|
|
|
|
// Override mime type if needed
|
|
if ( s.mimeType && xhr.overrideMimeType ) {
|
|
xhr.overrideMimeType( s.mimeType );
|
|
}
|
|
|
|
// X-Requested-With header
|
|
// For cross-domain requests, seeing as conditions for a preflight are
|
|
// akin to a jigsaw puzzle, we simply never set it to be sure.
|
|
// (it can always be set on a per-request basis or even using ajaxSetup)
|
|
// For same-domain requests, won't change header if already provided.
|
|
if ( !s.crossDomain && !headers["X-Requested-With"] ) {
|
|
headers["X-Requested-With"] = "XMLHttpRequest";
|
|
}
|
|
|
|
// Need an extra try/catch for cross domain requests in Firefox 3
|
|
try {
|
|
for ( i in headers ) {
|
|
xhr.setRequestHeader( i, headers[ i ] );
|
|
}
|
|
} catch( err ) {}
|
|
|
|
// Do send the request
|
|
// This may raise an exception which is actually
|
|
// handled in jQuery.ajax (so no try/catch here)
|
|
xhr.send( ( s.hasContent && s.data ) || null );
|
|
|
|
// Listener
|
|
callback = function( _, isAbort ) {
|
|
var status, responseHeaders, statusText, responses;
|
|
|
|
// Firefox throws exceptions when accessing properties
|
|
// of an xhr when a network error occurred
|
|
// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
|
|
try {
|
|
|
|
// Was never called and is aborted or complete
|
|
if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
|
|
|
|
// Only called once
|
|
callback = undefined;
|
|
|
|
// Do not keep as active anymore
|
|
if ( handle ) {
|
|
xhr.onreadystatechange = jQuery.noop;
|
|
if ( xhrOnUnloadAbort ) {
|
|
delete xhrCallbacks[ handle ];
|
|
}
|
|
}
|
|
|
|
// If it's an abort
|
|
if ( isAbort ) {
|
|
// Abort it manually if needed
|
|
if ( xhr.readyState !== 4 ) {
|
|
xhr.abort();
|
|
}
|
|
} else {
|
|
responses = {};
|
|
status = xhr.status;
|
|
responseHeaders = xhr.getAllResponseHeaders();
|
|
|
|
// When requesting binary data, IE6-9 will throw an exception
|
|
// on any attempt to access responseText (#11426)
|
|
if ( typeof xhr.responseText === "string" ) {
|
|
responses.text = xhr.responseText;
|
|
}
|
|
|
|
// Firefox throws an exception when accessing
|
|
// statusText for faulty cross-domain requests
|
|
try {
|
|
statusText = xhr.statusText;
|
|
} catch( e ) {
|
|
// We normalize with Webkit giving an empty statusText
|
|
statusText = "";
|
|
}
|
|
|
|
// Filter status for non standard behaviors
|
|
|
|
// If the request is local and we have data: assume a success
|
|
// (success with no data won't get notified, that's the best we
|
|
// can do given current implementations)
|
|
if ( !status && s.isLocal && !s.crossDomain ) {
|
|
status = responses.text ? 200 : 404;
|
|
// IE - #1450: sometimes returns 1223 when it should be 204
|
|
} else if ( status === 1223 ) {
|
|
status = 204;
|
|
}
|
|
}
|
|
}
|
|
} catch( firefoxAccessException ) {
|
|
if ( !isAbort ) {
|
|
complete( -1, firefoxAccessException );
|
|
}
|
|
}
|
|
|
|
// Call complete if needed
|
|
if ( responses ) {
|
|
complete( status, statusText, responses, responseHeaders );
|
|
}
|
|
};
|
|
|
|
if ( !s.async ) {
|
|
// if we're in sync mode we fire the callback
|
|
callback();
|
|
} else if ( xhr.readyState === 4 ) {
|
|
// (IE6 & IE7) if it's in cache and has been
|
|
// retrieved directly we need to fire the callback
|
|
setTimeout( callback );
|
|
} else {
|
|
handle = ++xhrId;
|
|
if ( xhrOnUnloadAbort ) {
|
|
// Create the active xhrs callbacks list if needed
|
|
// and attach the unload handler
|
|
if ( !xhrCallbacks ) {
|
|
xhrCallbacks = {};
|
|
jQuery( window ).unload( xhrOnUnloadAbort );
|
|
}
|
|
// Add to list of active xhrs callbacks
|
|
xhrCallbacks[ handle ] = callback;
|
|
}
|
|
xhr.onreadystatechange = callback;
|
|
}
|
|
},
|
|
|
|
abort: function() {
|
|
if ( callback ) {
|
|
callback( undefined, true );
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
var fxNow, timerId,
|
|
rfxtypes = /^(?:toggle|show|hide)$/,
|
|
rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
|
|
rrun = /queueHooks$/,
|
|
animationPrefilters = [ defaultPrefilter ],
|
|
tweeners = {
|
|
"*": [function( prop, value ) {
|
|
var tween = this.createTween( prop, value ),
|
|
target = tween.cur(),
|
|
parts = rfxnum.exec( value ),
|
|
unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
|
|
|
|
// Starting value computation is required for potential unit mismatches
|
|
start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) &&
|
|
rfxnum.exec( jQuery.css( tween.elem, prop ) ),
|
|
scale = 1,
|
|
maxIterations = 20;
|
|
|
|
if ( start && start[ 3 ] !== unit ) {
|
|
// Trust units reported by jQuery.css
|
|
unit = unit || start[ 3 ];
|
|
|
|
// Make sure we update the tween properties later on
|
|
parts = parts || [];
|
|
|
|
// Iteratively approximate from a nonzero starting point
|
|
start = +target || 1;
|
|
|
|
do {
|
|
// If previous iteration zeroed out, double until we get *something*
|
|
// Use a string for doubling factor so we don't accidentally see scale as unchanged below
|
|
scale = scale || ".5";
|
|
|
|
// Adjust and apply
|
|
start = start / scale;
|
|
jQuery.style( tween.elem, prop, start + unit );
|
|
|
|
// Update scale, tolerating zero or NaN from tween.cur()
|
|
// And breaking the loop if scale is unchanged or perfect, or if we've just had enough
|
|
} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
|
|
}
|
|
|
|
// Update tween properties
|
|
if ( parts ) {
|
|
start = tween.start = +start || +target || 0;
|
|
tween.unit = unit;
|
|
// If a +=/-= token was provided, we're doing a relative animation
|
|
tween.end = parts[ 1 ] ?
|
|
start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :
|
|
+parts[ 2 ];
|
|
}
|
|
|
|
return tween;
|
|
}]
|
|
};
|
|
|
|
// Animations created synchronously will run synchronously
|
|
function createFxNow() {
|
|
setTimeout(function() {
|
|
fxNow = undefined;
|
|
});
|
|
return ( fxNow = jQuery.now() );
|
|
}
|
|
|
|
function createTween( value, prop, animation ) {
|
|
var tween,
|
|
collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
|
|
index = 0,
|
|
length = collection.length;
|
|
for ( ; index < length; index++ ) {
|
|
if ( (tween = collection[ index ].call( animation, prop, value )) ) {
|
|
|
|
// we're done with this property
|
|
return tween;
|
|
}
|
|
}
|
|
}
|
|
|
|
function Animation( elem, properties, options ) {
|
|
var result,
|
|
stopped,
|
|
index = 0,
|
|
length = animationPrefilters.length,
|
|
deferred = jQuery.Deferred().always( function() {
|
|
// don't match elem in the :animated selector
|
|
delete tick.elem;
|
|
}),
|
|
tick = function() {
|
|
if ( stopped ) {
|
|
return false;
|
|
}
|
|
var currentTime = fxNow || createFxNow(),
|
|
remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
|
|
// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
|
|
temp = remaining / animation.duration || 0,
|
|
percent = 1 - temp,
|
|
index = 0,
|
|
length = animation.tweens.length;
|
|
|
|
for ( ; index < length ; index++ ) {
|
|
animation.tweens[ index ].run( percent );
|
|
}
|
|
|
|
deferred.notifyWith( elem, [ animation, percent, remaining ]);
|
|
|
|
if ( percent < 1 && length ) {
|
|
return remaining;
|
|
} else {
|
|
deferred.resolveWith( elem, [ animation ] );
|
|
return false;
|
|
}
|
|
},
|
|
animation = deferred.promise({
|
|
elem: elem,
|
|
props: jQuery.extend( {}, properties ),
|
|
opts: jQuery.extend( true, { specialEasing: {} }, options ),
|
|
originalProperties: properties,
|
|
originalOptions: options,
|
|
startTime: fxNow || createFxNow(),
|
|
duration: options.duration,
|
|
tweens: [],
|
|
createTween: function( prop, end ) {
|
|
var tween = jQuery.Tween( elem, animation.opts, prop, end,
|
|
animation.opts.specialEasing[ prop ] || animation.opts.easing );
|
|
animation.tweens.push( tween );
|
|
return tween;
|
|
},
|
|
stop: function( gotoEnd ) {
|
|
var index = 0,
|
|
// if we are going to the end, we want to run all the tweens
|
|
// otherwise we skip this part
|
|
length = gotoEnd ? animation.tweens.length : 0;
|
|
if ( stopped ) {
|
|
return this;
|
|
}
|
|
stopped = true;
|
|
for ( ; index < length ; index++ ) {
|
|
animation.tweens[ index ].run( 1 );
|
|
}
|
|
|
|
// resolve when we played the last frame
|
|
// otherwise, reject
|
|
if ( gotoEnd ) {
|
|
deferred.resolveWith( elem, [ animation, gotoEnd ] );
|
|
} else {
|
|
deferred.rejectWith( elem, [ animation, gotoEnd ] );
|
|
}
|
|
return this;
|
|
}
|
|
}),
|
|
props = animation.props;
|
|
|
|
propFilter( props, animation.opts.specialEasing );
|
|
|
|
for ( ; index < length ; index++ ) {
|
|
result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
|
|
if ( result ) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
jQuery.map( props, createTween, animation );
|
|
|
|
if ( jQuery.isFunction( animation.opts.start ) ) {
|
|
animation.opts.start.call( elem, animation );
|
|
}
|
|
|
|
jQuery.fx.timer(
|
|
jQuery.extend( tick, {
|
|
elem: elem,
|
|
anim: animation,
|
|
queue: animation.opts.queue
|
|
})
|
|
);
|
|
|
|
// attach callbacks from options
|
|
return animation.progress( animation.opts.progress )
|
|
.done( animation.opts.done, animation.opts.complete )
|
|
.fail( animation.opts.fail )
|
|
.always( animation.opts.always );
|
|
}
|
|
|
|
function propFilter( props, specialEasing ) {
|
|
var index, name, easing, value, hooks;
|
|
|
|
// camelCase, specialEasing and expand cssHook pass
|
|
for ( index in props ) {
|
|
name = jQuery.camelCase( index );
|
|
easing = specialEasing[ name ];
|
|
value = props[ index ];
|
|
if ( jQuery.isArray( value ) ) {
|
|
easing = value[ 1 ];
|
|
value = props[ index ] = value[ 0 ];
|
|
}
|
|
|
|
if ( index !== name ) {
|
|
props[ name ] = value;
|
|
delete props[ index ];
|
|
}
|
|
|
|
hooks = jQuery.cssHooks[ name ];
|
|
if ( hooks && "expand" in hooks ) {
|
|
value = hooks.expand( value );
|
|
delete props[ name ];
|
|
|
|
// not quite $.extend, this wont overwrite keys already present.
|
|
// also - reusing 'index' from above because we have the correct "name"
|
|
for ( index in value ) {
|
|
if ( !( index in props ) ) {
|
|
props[ index ] = value[ index ];
|
|
specialEasing[ index ] = easing;
|
|
}
|
|
}
|
|
} else {
|
|
specialEasing[ name ] = easing;
|
|
}
|
|
}
|
|
}
|
|
|
|
jQuery.Animation = jQuery.extend( Animation, {
|
|
|
|
tweener: function( props, callback ) {
|
|
if ( jQuery.isFunction( props ) ) {
|
|
callback = props;
|
|
props = [ "*" ];
|
|
} else {
|
|
props = props.split(" ");
|
|
}
|
|
|
|
var prop,
|
|
index = 0,
|
|
length = props.length;
|
|
|
|
for ( ; index < length ; index++ ) {
|
|
prop = props[ index ];
|
|
tweeners[ prop ] = tweeners[ prop ] || [];
|
|
tweeners[ prop ].unshift( callback );
|
|
}
|
|
},
|
|
|
|
prefilter: function( callback, prepend ) {
|
|
if ( prepend ) {
|
|
animationPrefilters.unshift( callback );
|
|
} else {
|
|
animationPrefilters.push( callback );
|
|
}
|
|
}
|
|
});
|
|
|
|
function defaultPrefilter( elem, props, opts ) {
|
|
/* jshint validthis: true */
|
|
var prop, value, toggle, tween, hooks, oldfire,
|
|
anim = this,
|
|
orig = {},
|
|
style = elem.style,
|
|
hidden = elem.nodeType && isHidden( elem ),
|
|
dataShow = jQuery._data( elem, "fxshow" );
|
|
|
|
// handle queue: false promises
|
|
if ( !opts.queue ) {
|
|
hooks = jQuery._queueHooks( elem, "fx" );
|
|
if ( hooks.unqueued == null ) {
|
|
hooks.unqueued = 0;
|
|
oldfire = hooks.empty.fire;
|
|
hooks.empty.fire = function() {
|
|
if ( !hooks.unqueued ) {
|
|
oldfire();
|
|
}
|
|
};
|
|
}
|
|
hooks.unqueued++;
|
|
|
|
anim.always(function() {
|
|
// doing this makes sure that the complete handler will be called
|
|
// before this completes
|
|
anim.always(function() {
|
|
hooks.unqueued--;
|
|
if ( !jQuery.queue( elem, "fx" ).length ) {
|
|
hooks.empty.fire();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// height/width overflow pass
|
|
if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
|
|
// Make sure that nothing sneaks out
|
|
// Record all 3 overflow attributes because IE does not
|
|
// change the overflow attribute when overflowX and
|
|
// overflowY are set to the same value
|
|
opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
|
|
|
|
// Set display property to inline-block for height/width
|
|
// animations on inline elements that are having width/height animated
|
|
if ( jQuery.css( elem, "display" ) === "inline" &&
|
|
jQuery.css( elem, "float" ) === "none" ) {
|
|
|
|
// inline-level elements accept inline-block;
|
|
// block-level elements need to be inline with layout
|
|
if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
|
|
style.display = "inline-block";
|
|
|
|
} else {
|
|
style.zoom = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( opts.overflow ) {
|
|
style.overflow = "hidden";
|
|
if ( !jQuery.support.shrinkWrapBlocks ) {
|
|
anim.always(function() {
|
|
style.overflow = opts.overflow[ 0 ];
|
|
style.overflowX = opts.overflow[ 1 ];
|
|
style.overflowY = opts.overflow[ 2 ];
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// show/hide pass
|
|
for ( prop in props ) {
|
|
value = props[ prop ];
|
|
if ( rfxtypes.exec( value ) ) {
|
|
delete props[ prop ];
|
|
toggle = toggle || value === "toggle";
|
|
if ( value === ( hidden ? "hide" : "show" ) ) {
|
|
continue;
|
|
}
|
|
orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
|
|
}
|
|
}
|
|
|
|
if ( !jQuery.isEmptyObject( orig ) ) {
|
|
if ( dataShow ) {
|
|
if ( "hidden" in dataShow ) {
|
|
hidden = dataShow.hidden;
|
|
}
|
|
} else {
|
|
dataShow = jQuery._data( elem, "fxshow", {} );
|
|
}
|
|
|
|
// store state if its toggle - enables .stop().toggle() to "reverse"
|
|
if ( toggle ) {
|
|
dataShow.hidden = !hidden;
|
|
}
|
|
if ( hidden ) {
|
|
jQuery( elem ).show();
|
|
} else {
|
|
anim.done(function() {
|
|
jQuery( elem ).hide();
|
|
});
|
|
}
|
|
anim.done(function() {
|
|
var prop;
|
|
jQuery._removeData( elem, "fxshow" );
|
|
for ( prop in orig ) {
|
|
jQuery.style( elem, prop, orig[ prop ] );
|
|
}
|
|
});
|
|
for ( prop in orig ) {
|
|
tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
|
|
|
|
if ( !( prop in dataShow ) ) {
|
|
dataShow[ prop ] = tween.start;
|
|
if ( hidden ) {
|
|
tween.end = tween.start;
|
|
tween.start = prop === "width" || prop === "height" ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Tween( elem, options, prop, end, easing ) {
|
|
return new Tween.prototype.init( elem, options, prop, end, easing );
|
|
}
|
|
jQuery.Tween = Tween;
|
|
|
|
Tween.prototype = {
|
|
constructor: Tween,
|
|
init: function( elem, options, prop, end, easing, unit ) {
|
|
this.elem = elem;
|
|
this.prop = prop;
|
|
this.easing = easing || "swing";
|
|
this.options = options;
|
|
this.start = this.now = this.cur();
|
|
this.end = end;
|
|
this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
|
|
},
|
|
cur: function() {
|
|
var hooks = Tween.propHooks[ this.prop ];
|
|
|
|
return hooks && hooks.get ?
|
|
hooks.get( this ) :
|
|
Tween.propHooks._default.get( this );
|
|
},
|
|
run: function( percent ) {
|
|
var eased,
|
|
hooks = Tween.propHooks[ this.prop ];
|
|
|
|
if ( this.options.duration ) {
|
|
this.pos = eased = jQuery.easing[ this.easing ](
|
|
percent, this.options.duration * percent, 0, 1, this.options.duration
|
|
);
|
|
} else {
|
|
this.pos = eased = percent;
|
|
}
|
|
this.now = ( this.end - this.start ) * eased + this.start;
|
|
|
|
if ( this.options.step ) {
|
|
this.options.step.call( this.elem, this.now, this );
|
|
}
|
|
|
|
if ( hooks && hooks.set ) {
|
|
hooks.set( this );
|
|
} else {
|
|
Tween.propHooks._default.set( this );
|
|
}
|
|
return this;
|
|
}
|
|
};
|
|
|
|
Tween.prototype.init.prototype = Tween.prototype;
|
|
|
|
Tween.propHooks = {
|
|
_default: {
|
|
get: function( tween ) {
|
|
var result;
|
|
|
|
if ( tween.elem[ tween.prop ] != null &&
|
|
(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
|
|
return tween.elem[ tween.prop ];
|
|
}
|
|
|
|
// passing an empty string as a 3rd parameter to .css will automatically
|
|
// attempt a parseFloat and fallback to a string if the parse fails
|
|
// so, simple values such as "10px" are parsed to Float.
|
|
// complex values such as "rotate(1rad)" are returned as is.
|
|
result = jQuery.css( tween.elem, tween.prop, "" );
|
|
// Empty strings, null, undefined and "auto" are converted to 0.
|
|
return !result || result === "auto" ? 0 : result;
|
|
},
|
|
set: function( tween ) {
|
|
// use step hook for back compat - use cssHook if its there - use .style if its
|
|
// available and use plain properties where available
|
|
if ( jQuery.fx.step[ tween.prop ] ) {
|
|
jQuery.fx.step[ tween.prop ]( tween );
|
|
} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
|
|
jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
|
|
} else {
|
|
tween.elem[ tween.prop ] = tween.now;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Support: IE <=9
|
|
// Panic based approach to setting things on disconnected nodes
|
|
|
|
Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
|
|
set: function( tween ) {
|
|
if ( tween.elem.nodeType && tween.elem.parentNode ) {
|
|
tween.elem[ tween.prop ] = tween.now;
|
|
}
|
|
}
|
|
};
|
|
|
|
jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
|
|
var cssFn = jQuery.fn[ name ];
|
|
jQuery.fn[ name ] = function( speed, easing, callback ) {
|
|
return speed == null || typeof speed === "boolean" ?
|
|
cssFn.apply( this, arguments ) :
|
|
this.animate( genFx( name, true ), speed, easing, callback );
|
|
};
|
|
});
|
|
|
|
jQuery.fn.extend({
|
|
fadeTo: function( speed, to, easing, callback ) {
|
|
|
|
// show any hidden elements after setting opacity to 0
|
|
return this.filter( isHidden ).css( "opacity", 0 ).show()
|
|
|
|
// animate to the value specified
|
|
.end().animate({ opacity: to }, speed, easing, callback );
|
|
},
|
|
animate: function( prop, speed, easing, callback ) {
|
|
var empty = jQuery.isEmptyObject( prop ),
|
|
optall = jQuery.speed( speed, easing, callback ),
|
|
doAnimation = function() {
|
|
// Operate on a copy of prop so per-property easing won't be lost
|
|
var anim = Animation( this, jQuery.extend( {}, prop ), optall );
|
|
|
|
// Empty animations, or finishing resolves immediately
|
|
if ( empty || jQuery._data( this, "finish" ) ) {
|
|
anim.stop( true );
|
|
}
|
|
};
|
|
doAnimation.finish = doAnimation;
|
|
|
|
return empty || optall.queue === false ?
|
|
this.each( doAnimation ) :
|
|
this.queue( optall.queue, doAnimation );
|
|
},
|
|
stop: function( type, clearQueue, gotoEnd ) {
|
|
var stopQueue = function( hooks ) {
|
|
var stop = hooks.stop;
|
|
delete hooks.stop;
|
|
stop( gotoEnd );
|
|
};
|
|
|
|
if ( typeof type !== "string" ) {
|
|
gotoEnd = clearQueue;
|
|
clearQueue = type;
|
|
type = undefined;
|
|
}
|
|
if ( clearQueue && type !== false ) {
|
|
this.queue( type || "fx", [] );
|
|
}
|
|
|
|
return this.each(function() {
|
|
var dequeue = true,
|
|
index = type != null && type + "queueHooks",
|
|
timers = jQuery.timers,
|
|
data = jQuery._data( this );
|
|
|
|
if ( index ) {
|
|
if ( data[ index ] && data[ index ].stop ) {
|
|
stopQueue( data[ index ] );
|
|
}
|
|
} else {
|
|
for ( index in data ) {
|
|
if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
|
|
stopQueue( data[ index ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
for ( index = timers.length; index--; ) {
|
|
if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
|
|
timers[ index ].anim.stop( gotoEnd );
|
|
dequeue = false;
|
|
timers.splice( index, 1 );
|
|
}
|
|
}
|
|
|
|
// start the next in the queue if the last step wasn't forced
|
|
// timers currently will call their complete callbacks, which will dequeue
|
|
// but only if they were gotoEnd
|
|
if ( dequeue || !gotoEnd ) {
|
|
jQuery.dequeue( this, type );
|
|
}
|
|
});
|
|
},
|
|
finish: function( type ) {
|
|
if ( type !== false ) {
|
|
type = type || "fx";
|
|
}
|
|
return this.each(function() {
|
|
var index,
|
|
data = jQuery._data( this ),
|
|
queue = data[ type + "queue" ],
|
|
hooks = data[ type + "queueHooks" ],
|
|
timers = jQuery.timers,
|
|
length = queue ? queue.length : 0;
|
|
|
|
// enable finishing flag on private data
|
|
data.finish = true;
|
|
|
|
// empty the queue first
|
|
jQuery.queue( this, type, [] );
|
|
|
|
if ( hooks && hooks.stop ) {
|
|
hooks.stop.call( this, true );
|
|
}
|
|
|
|
// look for any active animations, and finish them
|
|
for ( index = timers.length; index--; ) {
|
|
if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
|
|
timers[ index ].anim.stop( true );
|
|
timers.splice( index, 1 );
|
|
}
|
|
}
|
|
|
|
// look for any animations in the old queue and finish them
|
|
for ( index = 0; index < length; index++ ) {
|
|
if ( queue[ index ] && queue[ index ].finish ) {
|
|
queue[ index ].finish.call( this );
|
|
}
|
|
}
|
|
|
|
// turn off finishing flag
|
|
delete data.finish;
|
|
});
|
|
}
|
|
});
|
|
|
|
// Generate parameters to create a standard animation
|
|
function genFx( type, includeWidth ) {
|
|
var which,
|
|
attrs = { height: type },
|
|
i = 0;
|
|
|
|
// if we include width, step value is 1 to do all cssExpand values,
|
|
// if we don't include width, step value is 2 to skip over Left and Right
|
|
includeWidth = includeWidth? 1 : 0;
|
|
for( ; i < 4 ; i += 2 - includeWidth ) {
|
|
which = cssExpand[ i ];
|
|
attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
|
|
}
|
|
|
|
if ( includeWidth ) {
|
|
attrs.opacity = attrs.width = type;
|
|
}
|
|
|
|
return attrs;
|
|
}
|
|
|
|
// Generate shortcuts for custom animations
|
|
jQuery.each({
|
|
slideDown: genFx("show"),
|
|
slideUp: genFx("hide"),
|
|
slideToggle: genFx("toggle"),
|
|
fadeIn: { opacity: "show" },
|
|
fadeOut: { opacity: "hide" },
|
|
fadeToggle: { opacity: "toggle" }
|
|
}, function( name, props ) {
|
|
jQuery.fn[ name ] = function( speed, easing, callback ) {
|
|
return this.animate( props, speed, easing, callback );
|
|
};
|
|
});
|
|
|
|
jQuery.speed = function( speed, easing, fn ) {
|
|
var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
|
|
complete: fn || !fn && easing ||
|
|
jQuery.isFunction( speed ) && speed,
|
|
duration: speed,
|
|
easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
|
|
};
|
|
|
|
opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
|
|
opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
|
|
|
|
// normalize opt.queue - true/undefined/null -> "fx"
|
|
if ( opt.queue == null || opt.queue === true ) {
|
|
opt.queue = "fx";
|
|
}
|
|
|
|
// Queueing
|
|
opt.old = opt.complete;
|
|
|
|
opt.complete = function() {
|
|
if ( jQuery.isFunction( opt.old ) ) {
|
|
opt.old.call( this );
|
|
}
|
|
|
|
if ( opt.queue ) {
|
|
jQuery.dequeue( this, opt.queue );
|
|
}
|
|
};
|
|
|
|
return opt;
|
|
};
|
|
|
|
jQuery.easing = {
|
|
linear: function( p ) {
|
|
return p;
|
|
},
|
|
swing: function( p ) {
|
|
return 0.5 - Math.cos( p*Math.PI ) / 2;
|
|
}
|
|
};
|
|
|
|
jQuery.timers = [];
|
|
jQuery.fx = Tween.prototype.init;
|
|
jQuery.fx.tick = function() {
|
|
var timer,
|
|
timers = jQuery.timers,
|
|
i = 0;
|
|
|
|
fxNow = jQuery.now();
|
|
|
|
for ( ; i < timers.length; i++ ) {
|
|
timer = timers[ i ];
|
|
// Checks the timer has not already been removed
|
|
if ( !timer() && timers[ i ] === timer ) {
|
|
timers.splice( i--, 1 );
|
|
}
|
|
}
|
|
|
|
if ( !timers.length ) {
|
|
jQuery.fx.stop();
|
|
}
|
|
fxNow = undefined;
|
|
};
|
|
|
|
jQuery.fx.timer = function( timer ) {
|
|
if ( timer() && jQuery.timers.push( timer ) ) {
|
|
jQuery.fx.start();
|
|
}
|
|
};
|
|
|
|
jQuery.fx.interval = 13;
|
|
|
|
jQuery.fx.start = function() {
|
|
if ( !timerId ) {
|
|
timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
|
|
}
|
|
};
|
|
|
|
jQuery.fx.stop = function() {
|
|
clearInterval( timerId );
|
|
timerId = null;
|
|
};
|
|
|
|
jQuery.fx.speeds = {
|
|
slow: 600,
|
|
fast: 200,
|
|
// Default speed
|
|
_default: 400
|
|
};
|
|
|
|
// Back Compat <1.8 extension point
|
|
jQuery.fx.step = {};
|
|
|
|
if ( jQuery.expr && jQuery.expr.filters ) {
|
|
jQuery.expr.filters.animated = function( elem ) {
|
|
return jQuery.grep(jQuery.timers, function( fn ) {
|
|
return elem === fn.elem;
|
|
}).length;
|
|
};
|
|
}
|
|
jQuery.fn.offset = function( options ) {
|
|
if ( arguments.length ) {
|
|
return options === undefined ?
|
|
this :
|
|
this.each(function( i ) {
|
|
jQuery.offset.setOffset( this, options, i );
|
|
});
|
|
}
|
|
|
|
var docElem, win,
|
|
box = { top: 0, left: 0 },
|
|
elem = this[ 0 ],
|
|
doc = elem && elem.ownerDocument;
|
|
|
|
if ( !doc ) {
|
|
return;
|
|
}
|
|
|
|
docElem = doc.documentElement;
|
|
|
|
// Make sure it's not a disconnected DOM node
|
|
if ( !jQuery.contains( docElem, elem ) ) {
|
|
return box;
|
|
}
|
|
|
|
// If we don't have gBCR, just use 0,0 rather than error
|
|
// BlackBerry 5, iOS 3 (original iPhone)
|
|
if ( typeof elem.getBoundingClientRect !== core_strundefined ) {
|
|
box = elem.getBoundingClientRect();
|
|
}
|
|
win = getWindow( doc );
|
|
return {
|
|
top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ),
|
|
left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
|
|
};
|
|
};
|
|
|
|
jQuery.offset = {
|
|
|
|
setOffset: function( elem, options, i ) {
|
|
var position = jQuery.css( elem, "position" );
|
|
|
|
// set position first, in-case top/left are set even on static elem
|
|
if ( position === "static" ) {
|
|
elem.style.position = "relative";
|
|
}
|
|
|
|
var curElem = jQuery( elem ),
|
|
curOffset = curElem.offset(),
|
|
curCSSTop = jQuery.css( elem, "top" ),
|
|
curCSSLeft = jQuery.css( elem, "left" ),
|
|
calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
|
|
props = {}, curPosition = {}, curTop, curLeft;
|
|
|
|
// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
|
|
if ( calculatePosition ) {
|
|
curPosition = curElem.position();
|
|
curTop = curPosition.top;
|
|
curLeft = curPosition.left;
|
|
} else {
|
|
curTop = parseFloat( curCSSTop ) || 0;
|
|
curLeft = parseFloat( curCSSLeft ) || 0;
|
|
}
|
|
|
|
if ( jQuery.isFunction( options ) ) {
|
|
options = options.call( elem, i, curOffset );
|
|
}
|
|
|
|
if ( options.top != null ) {
|
|
props.top = ( options.top - curOffset.top ) + curTop;
|
|
}
|
|
if ( options.left != null ) {
|
|
props.left = ( options.left - curOffset.left ) + curLeft;
|
|
}
|
|
|
|
if ( "using" in options ) {
|
|
options.using.call( elem, props );
|
|
} else {
|
|
curElem.css( props );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
jQuery.fn.extend({
|
|
|
|
position: function() {
|
|
if ( !this[ 0 ] ) {
|
|
return;
|
|
}
|
|
|
|
var offsetParent, offset,
|
|
parentOffset = { top: 0, left: 0 },
|
|
elem = this[ 0 ];
|
|
|
|
// fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
|
|
if ( jQuery.css( elem, "position" ) === "fixed" ) {
|
|
// we assume that getBoundingClientRect is available when computed position is fixed
|
|
offset = elem.getBoundingClientRect();
|
|
} else {
|
|
// Get *real* offsetParent
|
|
offsetParent = this.offsetParent();
|
|
|
|
// Get correct offsets
|
|
offset = this.offset();
|
|
if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
|
|
parentOffset = offsetParent.offset();
|
|
}
|
|
|
|
// Add offsetParent borders
|
|
parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
|
|
parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
|
|
}
|
|
|
|
// Subtract parent offsets and element margins
|
|
// note: when an element has margin: auto the offsetLeft and marginLeft
|
|
// are the same in Safari causing offset.left to incorrectly be 0
|
|
return {
|
|
top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
|
|
left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)
|
|
};
|
|
},
|
|
|
|
offsetParent: function() {
|
|
return this.map(function() {
|
|
var offsetParent = this.offsetParent || docElem;
|
|
while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
|
|
offsetParent = offsetParent.offsetParent;
|
|
}
|
|
return offsetParent || docElem;
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
// Create scrollLeft and scrollTop methods
|
|
jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
|
|
var top = /Y/.test( prop );
|
|
|
|
jQuery.fn[ method ] = function( val ) {
|
|
return jQuery.access( this, function( elem, method, val ) {
|
|
var win = getWindow( elem );
|
|
|
|
if ( val === undefined ) {
|
|
return win ? (prop in win) ? win[ prop ] :
|
|
win.document.documentElement[ method ] :
|
|
elem[ method ];
|
|
}
|
|
|
|
if ( win ) {
|
|
win.scrollTo(
|
|
!top ? val : jQuery( win ).scrollLeft(),
|
|
top ? val : jQuery( win ).scrollTop()
|
|
);
|
|
|
|
} else {
|
|
elem[ method ] = val;
|
|
}
|
|
}, method, val, arguments.length, null );
|
|
};
|
|
});
|
|
|
|
function getWindow( elem ) {
|
|
return jQuery.isWindow( elem ) ?
|
|
elem :
|
|
elem.nodeType === 9 ?
|
|
elem.defaultView || elem.parentWindow :
|
|
false;
|
|
}
|
|
// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
|
|
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
|
|
jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
|
|
// margin is only for outerHeight, outerWidth
|
|
jQuery.fn[ funcName ] = function( margin, value ) {
|
|
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
|
|
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
|
|
|
|
return jQuery.access( this, function( elem, type, value ) {
|
|
var doc;
|
|
|
|
if ( jQuery.isWindow( elem ) ) {
|
|
// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
|
|
// isn't a whole lot we can do. See pull request at this URL for discussion:
|
|
// https://github.com/jquery/jquery/pull/764
|
|
return elem.document.documentElement[ "client" + name ];
|
|
}
|
|
|
|
// Get document width or height
|
|
if ( elem.nodeType === 9 ) {
|
|
doc = elem.documentElement;
|
|
|
|
// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
|
|
// unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
|
|
return Math.max(
|
|
elem.body[ "scroll" + name ], doc[ "scroll" + name ],
|
|
elem.body[ "offset" + name ], doc[ "offset" + name ],
|
|
doc[ "client" + name ]
|
|
);
|
|
}
|
|
|
|
return value === undefined ?
|
|
// Get width or height on the element, requesting but not forcing parseFloat
|
|
jQuery.css( elem, type, extra ) :
|
|
|
|
// Set width or height on the element
|
|
jQuery.style( elem, type, value, extra );
|
|
}, type, chainable ? margin : undefined, chainable, null );
|
|
};
|
|
});
|
|
});
|
|
// Limit scope pollution from any deprecated API
|
|
// (function() {
|
|
|
|
// The number of elements contained in the matched element set
|
|
jQuery.fn.size = function() {
|
|
return this.length;
|
|
};
|
|
|
|
jQuery.fn.andSelf = jQuery.fn.addBack;
|
|
|
|
// })();
|
|
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
|
// Expose jQuery as module.exports in loaders that implement the Node
|
|
// module pattern (including browserify). Do not create the global, since
|
|
// the user will be storing it themselves locally, and globals are frowned
|
|
// upon in the Node module world.
|
|
module.exports = jQuery;
|
|
} else {
|
|
// Otherwise expose jQuery to the global object as usual
|
|
window.jQuery = window.$ = jQuery;
|
|
|
|
// Register as a named AMD module, since jQuery can be concatenated with other
|
|
// files that may use define, but not via a proper concatenation script that
|
|
// understands anonymous AMD modules. A named AMD is safest and most robust
|
|
// way to register. Lowercase jquery is used because AMD module names are
|
|
// derived from file names, and jQuery is normally delivered in a lowercase
|
|
// file name. Do this after creating the global so that if an AMD module wants
|
|
// to call noConflict to hide this version of jQuery, it will work.
|
|
if ( typeof define === "function" && define.amd ) {
|
|
define( "jquery", [], function () { return jQuery; } );
|
|
}
|
|
}
|
|
|
|
})( window );
|
|
|
|
/**
|
|
* jQuery JSON plugin 2.4.0
|
|
*
|
|
* @author Brantley Harris, 2009-2011
|
|
* @author Timo Tijhof, 2011-2012
|
|
* @source This plugin is heavily influenced by MochiKit's serializeJSON, which is
|
|
* copyrighted 2005 by Bob Ippolito.
|
|
* @source Brantley Harris wrote this plugin. It is based somewhat on the JSON.org
|
|
* website's http://www.json.org/json2.js, which proclaims:
|
|
* "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
|
|
* I uphold.
|
|
* @license MIT License <http://www.opensource.org/licenses/mit-license.php>
|
|
*/
|
|
(function ($) {
|
|
'use strict';
|
|
|
|
var escape = /["\\\x00-\x1f\x7f-\x9f]/g,
|
|
meta = {
|
|
'\b': '\\b',
|
|
'\t': '\\t',
|
|
'\n': '\\n',
|
|
'\f': '\\f',
|
|
'\r': '\\r',
|
|
'"' : '\\"',
|
|
'\\': '\\\\'
|
|
},
|
|
hasOwn = Object.prototype.hasOwnProperty;
|
|
|
|
/**
|
|
* jQuery.toJSON
|
|
* Converts the given argument into a JSON representation.
|
|
*
|
|
* @param o {Mixed} The json-serializable *thing* to be converted
|
|
*
|
|
* If an object has a toJSON prototype, that will be used to get the representation.
|
|
* Non-integer/string keys are skipped in the object, as are keys that point to a
|
|
* function.
|
|
*
|
|
*/
|
|
$.toJSON = typeof JSON === 'object' && JSON.stringify ? JSON.stringify : function (o) {
|
|
if (o === null) {
|
|
return 'null';
|
|
}
|
|
|
|
var pairs, k, name, val,
|
|
type = $.type(o);
|
|
|
|
if (type === 'undefined') {
|
|
return undefined;
|
|
}
|
|
|
|
// Also covers instantiated Number and Boolean objects,
|
|
// which are typeof 'object' but thanks to $.type, we
|
|
// catch them here. I don't know whether it is right
|
|
// or wrong that instantiated primitives are not
|
|
// exported to JSON as an {"object":..}.
|
|
// We choose this path because that's what the browsers did.
|
|
if (type === 'number' || type === 'boolean') {
|
|
return String(o);
|
|
}
|
|
if (type === 'string') {
|
|
return $.quoteString(o);
|
|
}
|
|
if (typeof o.toJSON === 'function') {
|
|
return $.toJSON(o.toJSON());
|
|
}
|
|
if (type === 'date') {
|
|
var month = o.getUTCMonth() + 1,
|
|
day = o.getUTCDate(),
|
|
year = o.getUTCFullYear(),
|
|
hours = o.getUTCHours(),
|
|
minutes = o.getUTCMinutes(),
|
|
seconds = o.getUTCSeconds(),
|
|
milli = o.getUTCMilliseconds();
|
|
|
|
if (month < 10) {
|
|
month = '0' + month;
|
|
}
|
|
if (day < 10) {
|
|
day = '0' + day;
|
|
}
|
|
if (hours < 10) {
|
|
hours = '0' + hours;
|
|
}
|
|
if (minutes < 10) {
|
|
minutes = '0' + minutes;
|
|
}
|
|
if (seconds < 10) {
|
|
seconds = '0' + seconds;
|
|
}
|
|
if (milli < 100) {
|
|
milli = '0' + milli;
|
|
}
|
|
if (milli < 10) {
|
|
milli = '0' + milli;
|
|
}
|
|
return '"' + year + '-' + month + '-' + day + 'T' +
|
|
hours + ':' + minutes + ':' + seconds +
|
|
'.' + milli + 'Z"';
|
|
}
|
|
|
|
pairs = [];
|
|
|
|
if ($.isArray(o)) {
|
|
for (k = 0; k < o.length; k++) {
|
|
pairs.push($.toJSON(o[k]) || 'null');
|
|
}
|
|
return '[' + pairs.join(',') + ']';
|
|
}
|
|
|
|
// Any other object (plain object, RegExp, ..)
|
|
// Need to do typeof instead of $.type, because we also
|
|
// want to catch non-plain objects.
|
|
if (typeof o === 'object') {
|
|
for (k in o) {
|
|
// Only include own properties,
|
|
// Filter out inherited prototypes
|
|
if (hasOwn.call(o, k)) {
|
|
// Keys must be numerical or string. Skip others
|
|
type = typeof k;
|
|
if (type === 'number') {
|
|
name = '"' + k + '"';
|
|
} else if (type === 'string') {
|
|
name = $.quoteString(k);
|
|
} else {
|
|
continue;
|
|
}
|
|
type = typeof o[k];
|
|
|
|
// Invalid values like these return undefined
|
|
// from toJSON, however those object members
|
|
// shouldn't be included in the JSON string at all.
|
|
if (type !== 'function' && type !== 'undefined') {
|
|
val = $.toJSON(o[k]);
|
|
pairs.push(name + ':' + val);
|
|
}
|
|
}
|
|
}
|
|
return '{' + pairs.join(',') + '}';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* jQuery.evalJSON
|
|
* Evaluates a given json string.
|
|
*
|
|
* @param str {String}
|
|
*/
|
|
$.evalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) {
|
|
/*jshint evil: true */
|
|
return eval('(' + str + ')');
|
|
};
|
|
|
|
/**
|
|
* jQuery.secureEvalJSON
|
|
* Evals JSON in a way that is *more* secure.
|
|
*
|
|
* @param str {String}
|
|
*/
|
|
$.secureEvalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) {
|
|
var filtered =
|
|
str
|
|
.replace(/\\["\\\/bfnrtu]/g, '@')
|
|
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
|
|
.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
|
|
|
|
if (/^[\],:{}\s]*$/.test(filtered)) {
|
|
/*jshint evil: true */
|
|
return eval('(' + str + ')');
|
|
}
|
|
throw new SyntaxError('Error parsing JSON, source is not valid.');
|
|
};
|
|
|
|
/**
|
|
* jQuery.quoteString
|
|
* Returns a string-repr of a string, escaping quotes intelligently.
|
|
* Mostly a support function for toJSON.
|
|
* Examples:
|
|
* >>> jQuery.quoteString('apple')
|
|
* "apple"
|
|
*
|
|
* >>> jQuery.quoteString('"Where are we going?", she asked.')
|
|
* "\"Where are we going?\", she asked."
|
|
*/
|
|
$.quoteString = function (str) {
|
|
if (str.match(escape)) {
|
|
return '"' + str.replace(escape, function (a) {
|
|
var c = meta[a];
|
|
if (typeof c === 'string') {
|
|
return c;
|
|
}
|
|
c = a.charCodeAt();
|
|
return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
|
|
}) + '"';
|
|
}
|
|
return '"' + str + '"';
|
|
};
|
|
|
|
}(jQuery));
|
|
|
|
/*!
|
|
* jQuery Form Plugin
|
|
* version: 3.27.0-2013.02.06
|
|
* @requires jQuery v1.5 or later
|
|
*
|
|
* Examples and documentation at: http://malsup.com/jquery/form/
|
|
* Project repository: https://github.com/malsup/form
|
|
* Dual licensed under the MIT and GPL licenses:
|
|
* http://malsup.github.com/mit-license.txt
|
|
* http://malsup.github.com/gpl-license-v2.txt
|
|
*/
|
|
/*global ActiveXObject alert */
|
|
;(function($) {
|
|
"use strict";
|
|
|
|
/*
|
|
Usage Note:
|
|
-----------
|
|
Do not use both ajaxSubmit and ajaxForm on the same form. These
|
|
functions are mutually exclusive. Use ajaxSubmit if you want
|
|
to bind your own submit handler to the form. For example,
|
|
|
|
$(document).ready(function() {
|
|
$('#myForm').on('submit', function(e) {
|
|
e.preventDefault(); // <-- important
|
|
$(this).ajaxSubmit({
|
|
target: '#output'
|
|
});
|
|
});
|
|
});
|
|
|
|
Use ajaxForm when you want the plugin to manage all the event binding
|
|
for you. For example,
|
|
|
|
$(document).ready(function() {
|
|
$('#myForm').ajaxForm({
|
|
target: '#output'
|
|
});
|
|
});
|
|
|
|
You can also use ajaxForm with delegation (requires jQuery v1.7+), so the
|
|
form does not have to exist when you invoke ajaxForm:
|
|
|
|
$('#myForm').ajaxForm({
|
|
delegation: true,
|
|
target: '#output'
|
|
});
|
|
|
|
When using ajaxForm, the ajaxSubmit function will be invoked for you
|
|
at the appropriate time.
|
|
*/
|
|
|
|
/**
|
|
* Feature detection
|
|
*/
|
|
var feature = {};
|
|
feature.fileapi = $("<input type='file'/>").get(0).files !== undefined;
|
|
feature.formdata = window.FormData !== undefined;
|
|
|
|
/**
|
|
* ajaxSubmit() provides a mechanism for immediately submitting
|
|
* an HTML form using AJAX.
|
|
*/
|
|
$.fn.ajaxSubmit = function(options) {
|
|
/*jshint scripturl:true */
|
|
|
|
// fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
|
|
if (!this.length) {
|
|
log('ajaxSubmit: skipping submit process - no element selected');
|
|
return this;
|
|
}
|
|
|
|
var method, action, url, $form = this;
|
|
|
|
if (typeof options == 'function') {
|
|
options = { success: options };
|
|
}
|
|
|
|
method = this.attr('method');
|
|
action = this.attr('action');
|
|
url = (typeof action === 'string') ? $.trim(action) : '';
|
|
url = url || window.location.href || '';
|
|
if (url) {
|
|
// clean url (don't include hash vaue)
|
|
url = (url.match(/^([^#]+)/)||[])[1];
|
|
}
|
|
|
|
options = $.extend(true, {
|
|
url: url,
|
|
success: $.ajaxSettings.success,
|
|
type: method || 'GET',
|
|
iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
|
|
}, options);
|
|
|
|
// hook for manipulating the form data before it is extracted;
|
|
// convenient for use with rich editors like tinyMCE or FCKEditor
|
|
var veto = {};
|
|
this.trigger('form-pre-serialize', [this, options, veto]);
|
|
if (veto.veto) {
|
|
log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
|
|
return this;
|
|
}
|
|
|
|
// provide opportunity to alter form data before it is serialized
|
|
if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
|
|
log('ajaxSubmit: submit aborted via beforeSerialize callback');
|
|
return this;
|
|
}
|
|
|
|
var traditional = options.traditional;
|
|
if ( traditional === undefined ) {
|
|
traditional = $.ajaxSettings.traditional;
|
|
}
|
|
|
|
var elements = [];
|
|
var qx, a = this.formToArray(options.semantic, elements);
|
|
if (options.data) {
|
|
options.extraData = options.data;
|
|
qx = $.param(options.data, traditional);
|
|
}
|
|
|
|
// give pre-submit callback an opportunity to abort the submit
|
|
if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
|
|
log('ajaxSubmit: submit aborted via beforeSubmit callback');
|
|
return this;
|
|
}
|
|
|
|
// fire vetoable 'validate' event
|
|
this.trigger('form-submit-validate', [a, this, options, veto]);
|
|
if (veto.veto) {
|
|
log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
|
|
return this;
|
|
}
|
|
|
|
var q = $.param(a, traditional);
|
|
if (qx) {
|
|
q = ( q ? (q + '&' + qx) : qx );
|
|
}
|
|
if (options.type.toUpperCase() == 'GET') {
|
|
options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
|
|
options.data = null; // data is null for 'get'
|
|
}
|
|
else {
|
|
options.data = q; // data is the query string for 'post'
|
|
}
|
|
|
|
var callbacks = [];
|
|
if (options.resetForm) {
|
|
callbacks.push(function() { $form.resetForm(); });
|
|
}
|
|
if (options.clearForm) {
|
|
callbacks.push(function() { $form.clearForm(options.includeHidden); });
|
|
}
|
|
|
|
// perform a load on the target only if dataType is not provided
|
|
if (!options.dataType && options.target) {
|
|
var oldSuccess = options.success || function(){};
|
|
callbacks.push(function(data) {
|
|
var fn = options.replaceTarget ? 'replaceWith' : 'html';
|
|
$(options.target)[fn](data).each(oldSuccess, arguments);
|
|
});
|
|
}
|
|
else if (options.success) {
|
|
callbacks.push(options.success);
|
|
}
|
|
|
|
options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
|
|
var context = options.context || this ; // jQuery 1.4+ supports scope context
|
|
for (var i=0, max=callbacks.length; i < max; i++) {
|
|
callbacks[i].apply(context, [data, status, xhr || $form, $form]);
|
|
}
|
|
};
|
|
|
|
// are there files to upload?
|
|
|
|
// [value] (issue #113), also see comment:
|
|
// https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219
|
|
var fileInputs = $('input[type=file]:enabled[value!=""]', this);
|
|
|
|
var hasFileInputs = fileInputs.length > 0;
|
|
var mp = 'multipart/form-data';
|
|
var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
|
|
|
|
var fileAPI = feature.fileapi && feature.formdata;
|
|
log("fileAPI :" + fileAPI);
|
|
var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
|
|
|
|
var jqxhr;
|
|
|
|
// options.iframe allows user to force iframe mode
|
|
// 06-NOV-09: now defaulting to iframe mode if file input is detected
|
|
if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
|
|
// hack to fix Safari hang (thanks to Tim Molendijk for this)
|
|
// see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
|
|
if (options.closeKeepAlive) {
|
|
$.get(options.closeKeepAlive, function() {
|
|
jqxhr = fileUploadIframe(a);
|
|
});
|
|
}
|
|
else {
|
|
jqxhr = fileUploadIframe(a);
|
|
}
|
|
}
|
|
else if ((hasFileInputs || multipart) && fileAPI) {
|
|
jqxhr = fileUploadXhr(a);
|
|
}
|
|
else {
|
|
jqxhr = $.ajax(options);
|
|
}
|
|
|
|
$form.removeData('jqxhr').data('jqxhr', jqxhr);
|
|
|
|
// clear element array
|
|
for (var k=0; k < elements.length; k++)
|
|
elements[k] = null;
|
|
|
|
// fire 'notify' event
|
|
this.trigger('form-submit-notify', [this, options]);
|
|
return this;
|
|
|
|
// utility fn for deep serialization
|
|
function deepSerialize(extraData){
|
|
var serialized = $.param(extraData).split('&');
|
|
var len = serialized.length;
|
|
var result = [];
|
|
var i, part;
|
|
for (i=0; i < len; i++) {
|
|
// #252; undo param space replacement
|
|
serialized[i] = serialized[i].replace(/\+/g,' ');
|
|
part = serialized[i].split('=');
|
|
// #278; use array instead of object storage, favoring array serializations
|
|
result.push([decodeURIComponent(part[0]), decodeURIComponent(part[1])]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
|
|
function fileUploadXhr(a) {
|
|
var formdata = new FormData();
|
|
|
|
for (var i=0; i < a.length; i++) {
|
|
formdata.append(a[i].name, a[i].value);
|
|
}
|
|
|
|
if (options.extraData) {
|
|
var serializedData = deepSerialize(options.extraData);
|
|
for (i=0; i < serializedData.length; i++)
|
|
if (serializedData[i])
|
|
formdata.append(serializedData[i][0], serializedData[i][1]);
|
|
}
|
|
|
|
options.data = null;
|
|
|
|
var s = $.extend(true, {}, $.ajaxSettings, options, {
|
|
contentType: false,
|
|
processData: false,
|
|
cache: false,
|
|
type: method || 'POST'
|
|
});
|
|
|
|
if (options.uploadProgress) {
|
|
// workaround because jqXHR does not expose upload property
|
|
s.xhr = function() {
|
|
var xhr = jQuery.ajaxSettings.xhr();
|
|
if (xhr.upload) {
|
|
xhr.upload.addEventListener('progress', function(event) {
|
|
var percent = 0;
|
|
var position = event.loaded || event.position; /*event.position is deprecated*/
|
|
var total = event.total;
|
|
if (event.lengthComputable) {
|
|
percent = Math.ceil(position / total * 100);
|
|
}
|
|
options.uploadProgress(event, position, total, percent);
|
|
}, false);
|
|
}
|
|
return xhr;
|
|
};
|
|
}
|
|
|
|
s.data = null;
|
|
var beforeSend = s.beforeSend;
|
|
s.beforeSend = function(xhr, o) {
|
|
o.data = formdata;
|
|
if(beforeSend)
|
|
beforeSend.call(this, xhr, o);
|
|
};
|
|
return $.ajax(s);
|
|
}
|
|
|
|
// private function for handling file uploads (hat tip to YAHOO!)
|
|
function fileUploadIframe(a) {
|
|
var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
|
|
var useProp = !!$.fn.prop;
|
|
var deferred = $.Deferred();
|
|
|
|
if (a) {
|
|
// ensure that every serialized input is still enabled
|
|
for (i=0; i < elements.length; i++) {
|
|
el = $(elements[i]);
|
|
if ( useProp )
|
|
el.prop('disabled', false);
|
|
else
|
|
el.removeAttr('disabled');
|
|
}
|
|
}
|
|
|
|
s = $.extend(true, {}, $.ajaxSettings, options);
|
|
s.context = s.context || s;
|
|
id = 'jqFormIO' + (new Date().getTime());
|
|
if (s.iframeTarget) {
|
|
$io = $(s.iframeTarget);
|
|
n = $io.attr('name');
|
|
if (!n)
|
|
$io.attr('name', id);
|
|
else
|
|
id = n;
|
|
}
|
|
else {
|
|
$io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
|
|
$io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
|
|
}
|
|
io = $io[0];
|
|
|
|
|
|
xhr = { // mock object
|
|
aborted: 0,
|
|
responseText: null,
|
|
responseXML: null,
|
|
status: 0,
|
|
statusText: 'n/a',
|
|
getAllResponseHeaders: function() {},
|
|
getResponseHeader: function() {},
|
|
setRequestHeader: function() {},
|
|
abort: function(status) {
|
|
var e = (status === 'timeout' ? 'timeout' : 'aborted');
|
|
log('aborting upload... ' + e);
|
|
this.aborted = 1;
|
|
|
|
try { // #214, #257
|
|
if (io.contentWindow.document.execCommand) {
|
|
io.contentWindow.document.execCommand('Stop');
|
|
}
|
|
}
|
|
catch(ignore) {}
|
|
|
|
$io.attr('src', s.iframeSrc); // abort op in progress
|
|
xhr.error = e;
|
|
if (s.error)
|
|
s.error.call(s.context, xhr, e, status);
|
|
if (g)
|
|
$.event.trigger("ajaxError", [xhr, s, e]);
|
|
if (s.complete)
|
|
s.complete.call(s.context, xhr, e);
|
|
}
|
|
};
|
|
|
|
g = s.global;
|
|
// trigger ajax global events so that activity/block indicators work like normal
|
|
if (g && 0 === $.active++) {
|
|
$.event.trigger("ajaxStart");
|
|
}
|
|
if (g) {
|
|
$.event.trigger("ajaxSend", [xhr, s]);
|
|
}
|
|
|
|
if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
|
|
if (s.global) {
|
|
$.active--;
|
|
}
|
|
deferred.reject();
|
|
return deferred;
|
|
}
|
|
if (xhr.aborted) {
|
|
deferred.reject();
|
|
return deferred;
|
|
}
|
|
|
|
// add submitting element to data if we know it
|
|
sub = form.clk;
|
|
if (sub) {
|
|
n = sub.name;
|
|
if (n && !sub.disabled) {
|
|
s.extraData = s.extraData || {};
|
|
s.extraData[n] = sub.value;
|
|
if (sub.type == "image") {
|
|
s.extraData[n+'.x'] = form.clk_x;
|
|
s.extraData[n+'.y'] = form.clk_y;
|
|
}
|
|
}
|
|
}
|
|
|
|
var CLIENT_TIMEOUT_ABORT = 1;
|
|
var SERVER_ABORT = 2;
|
|
|
|
function getDoc(frame) {
|
|
var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
|
|
return doc;
|
|
}
|
|
|
|
// Rails CSRF hack (thanks to Yvan Barthelemy)
|
|
var csrf_token = $('meta[name=csrf-token]').attr('content');
|
|
var csrf_param = $('meta[name=csrf-param]').attr('content');
|
|
if (csrf_param && csrf_token) {
|
|
s.extraData = s.extraData || {};
|
|
s.extraData[csrf_param] = csrf_token;
|
|
}
|
|
|
|
// take a breath so that pending repaints get some cpu time before the upload starts
|
|
function doSubmit() {
|
|
// make sure form attrs are set
|
|
var t = $form.attr('target'), a = $form.attr('action');
|
|
|
|
// update form attrs in IE friendly way
|
|
form.setAttribute('target',id);
|
|
if (!method) {
|
|
form.setAttribute('method', 'POST');
|
|
}
|
|
if (a != s.url) {
|
|
form.setAttribute('action', s.url);
|
|
}
|
|
|
|
// ie borks in some cases when setting encoding
|
|
if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
|
|
$form.attr({
|
|
encoding: 'multipart/form-data',
|
|
enctype: 'multipart/form-data'
|
|
});
|
|
}
|
|
|
|
// support timout
|
|
if (s.timeout) {
|
|
timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
|
|
}
|
|
|
|
// look for server aborts
|
|
function checkState() {
|
|
try {
|
|
var state = getDoc(io).readyState;
|
|
log('state = ' + state);
|
|
if (state && state.toLowerCase() == 'uninitialized')
|
|
setTimeout(checkState,50);
|
|
}
|
|
catch(e) {
|
|
log('Server abort: ' , e, ' (', e.name, ')');
|
|
cb(SERVER_ABORT);
|
|
if (timeoutHandle)
|
|
clearTimeout(timeoutHandle);
|
|
timeoutHandle = undefined;
|
|
}
|
|
}
|
|
|
|
// add "extra" data to form if provided in options
|
|
var extraInputs = [];
|
|
try {
|
|
if (s.extraData) {
|
|
for (var n in s.extraData) {
|
|
if (s.extraData.hasOwnProperty(n)) {
|
|
// if using the $.param format that allows for multiple values with the same name
|
|
if($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) {
|
|
extraInputs.push(
|
|
$('<input type="hidden" name="'+s.extraData[n].name+'">').val(s.extraData[n].value)
|
|
.appendTo(form)[0]);
|
|
} else {
|
|
extraInputs.push(
|
|
$('<input type="hidden" name="'+n+'">').val(s.extraData[n])
|
|
.appendTo(form)[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!s.iframeTarget) {
|
|
// add iframe to doc and submit the form
|
|
$io.appendTo('body');
|
|
if (io.attachEvent)
|
|
io.attachEvent('onload', cb);
|
|
else
|
|
io.addEventListener('load', cb, false);
|
|
}
|
|
setTimeout(checkState,15);
|
|
// just in case form has element with name/id of 'submit'
|
|
var submitFn = document.createElement('form').submit;
|
|
submitFn.apply(form);
|
|
}
|
|
finally {
|
|
// reset attrs and remove "extra" input elements
|
|
form.setAttribute('action',a);
|
|
if(t) {
|
|
form.setAttribute('target', t);
|
|
} else {
|
|
$form.removeAttr('target');
|
|
}
|
|
$(extraInputs).remove();
|
|
}
|
|
}
|
|
|
|
if (s.forceSync) {
|
|
doSubmit();
|
|
}
|
|
else {
|
|
setTimeout(doSubmit, 10); // this lets dom updates render
|
|
}
|
|
|
|
var data, doc, domCheckCount = 50, callbackProcessed;
|
|
|
|
function cb(e) {
|
|
if (xhr.aborted || callbackProcessed) {
|
|
return;
|
|
}
|
|
try {
|
|
doc = getDoc(io);
|
|
}
|
|
catch(ex) {
|
|
log('cannot access response document: ', ex);
|
|
e = SERVER_ABORT;
|
|
}
|
|
if (e === CLIENT_TIMEOUT_ABORT && xhr) {
|
|
xhr.abort('timeout');
|
|
deferred.reject(xhr, 'timeout');
|
|
return;
|
|
}
|
|
else if (e == SERVER_ABORT && xhr) {
|
|
xhr.abort('server abort');
|
|
deferred.reject(xhr, 'error', 'server abort');
|
|
return;
|
|
}
|
|
|
|
if (!doc || doc.location.href == s.iframeSrc) {
|
|
// response not received yet
|
|
if (!timedOut)
|
|
return;
|
|
}
|
|
if (io.detachEvent)
|
|
io.detachEvent('onload', cb);
|
|
else
|
|
io.removeEventListener('load', cb, false);
|
|
|
|
var status = 'success', errMsg;
|
|
try {
|
|
if (timedOut) {
|
|
throw 'timeout';
|
|
}
|
|
|
|
var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
|
|
log('isXml='+isXml);
|
|
if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) {
|
|
if (--domCheckCount) {
|
|
// in some browsers (Opera) the iframe DOM is not always traversable when
|
|
// the onload callback fires, so we loop a bit to accommodate
|
|
log('requeing onLoad callback, DOM not available');
|
|
setTimeout(cb, 250);
|
|
return;
|
|
}
|
|
// let this fall through because server response could be an empty document
|
|
//log('Could not access iframe DOM after mutiple tries.');
|
|
//throw 'DOMException: not available';
|
|
}
|
|
|
|
//log('response detected');
|
|
var docRoot = doc.body ? doc.body : doc.documentElement;
|
|
xhr.responseText = docRoot ? docRoot.innerHTML : null;
|
|
xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
|
|
if (isXml)
|
|
s.dataType = 'xml';
|
|
xhr.getResponseHeader = function(header){
|
|
var headers = {'content-type': s.dataType};
|
|
return headers[header];
|
|
};
|
|
// support for XHR 'status' & 'statusText' emulation :
|
|
if (docRoot) {
|
|
xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
|
|
xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
|
|
}
|
|
|
|
var dt = (s.dataType || '').toLowerCase();
|
|
var scr = /(json|script|text)/.test(dt);
|
|
if (scr || s.textarea) {
|
|
// see if user embedded response in textarea
|
|
var ta = doc.getElementsByTagName('textarea')[0];
|
|
if (ta) {
|
|
xhr.responseText = ta.value;
|
|
// support for XHR 'status' & 'statusText' emulation :
|
|
xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
|
|
xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
|
|
}
|
|
else if (scr) {
|
|
// account for browsers injecting pre around json response
|
|
var pre = doc.getElementsByTagName('pre')[0];
|
|
var b = doc.getElementsByTagName('body')[0];
|
|
if (pre) {
|
|
xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
|
|
}
|
|
else if (b) {
|
|
xhr.responseText = b.textContent ? b.textContent : b.innerText;
|
|
}
|
|
}
|
|
}
|
|
else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) {
|
|
xhr.responseXML = toXml(xhr.responseText);
|
|
}
|
|
|
|
try {
|
|
data = httpData(xhr, dt, s);
|
|
}
|
|
catch (e) {
|
|
status = 'parsererror';
|
|
xhr.error = errMsg = (e || status);
|
|
}
|
|
}
|
|
catch (e) {
|
|
log('error caught: ',e);
|
|
status = 'error';
|
|
xhr.error = errMsg = (e || status);
|
|
}
|
|
|
|
if (xhr.aborted) {
|
|
log('upload aborted');
|
|
status = null;
|
|
}
|
|
|
|
if (xhr.status) { // we've set xhr.status
|
|
status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
|
|
}
|
|
|
|
// ordering of these callbacks/triggers is odd, but that's how $.ajax does it
|
|
if (status === 'success') {
|
|
if (s.success)
|
|
s.success.call(s.context, data, 'success', xhr);
|
|
deferred.resolve(xhr.responseText, 'success', xhr);
|
|
if (g)
|
|
$.event.trigger("ajaxSuccess", [xhr, s]);
|
|
}
|
|
else if (status) {
|
|
if (errMsg === undefined)
|
|
errMsg = xhr.statusText;
|
|
if (s.error)
|
|
s.error.call(s.context, xhr, status, errMsg);
|
|
deferred.reject(xhr, 'error', errMsg);
|
|
if (g)
|
|
$.event.trigger("ajaxError", [xhr, s, errMsg]);
|
|
}
|
|
|
|
if (g)
|
|
$.event.trigger("ajaxComplete", [xhr, s]);
|
|
|
|
if (g && ! --$.active) {
|
|
$.event.trigger("ajaxStop");
|
|
}
|
|
|
|
if (s.complete)
|
|
s.complete.call(s.context, xhr, status);
|
|
|
|
callbackProcessed = true;
|
|
if (s.timeout)
|
|
clearTimeout(timeoutHandle);
|
|
|
|
// clean up
|
|
setTimeout(function() {
|
|
if (!s.iframeTarget)
|
|
$io.remove();
|
|
xhr.responseXML = null;
|
|
}, 100);
|
|
}
|
|
|
|
var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
|
|
if (window.ActiveXObject) {
|
|
doc = new ActiveXObject('Microsoft.XMLDOM');
|
|
doc.async = 'false';
|
|
doc.loadXML(s);
|
|
}
|
|
else {
|
|
doc = (new DOMParser()).parseFromString(s, 'text/xml');
|
|
}
|
|
return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
|
|
};
|
|
var parseJSON = $.parseJSON || function(s) {
|
|
/*jslint evil:true */
|
|
return window['eval']('(' + s + ')');
|
|
};
|
|
|
|
var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
|
|
|
|
var ct = xhr.getResponseHeader('content-type') || '',
|
|
xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
|
|
data = xml ? xhr.responseXML : xhr.responseText;
|
|
|
|
if (xml && data.documentElement.nodeName === 'parsererror') {
|
|
if ($.error)
|
|
$.error('parsererror');
|
|
}
|
|
if (s && s.dataFilter) {
|
|
data = s.dataFilter(data, type);
|
|
}
|
|
if (typeof data === 'string') {
|
|
if (type === 'json' || !type && ct.indexOf('json') >= 0) {
|
|
data = parseJSON(data);
|
|
} else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
|
|
$.globalEval(data);
|
|
}
|
|
}
|
|
return data;
|
|
};
|
|
|
|
return deferred;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ajaxForm() provides a mechanism for fully automating form submission.
|
|
*
|
|
* The advantages of using this method instead of ajaxSubmit() are:
|
|
*
|
|
* 1: This method will include coordinates for <input type="image" /> elements (if the element
|
|
* is used to submit the form).
|
|
* 2. This method will include the submit element's name/value data (for the element that was
|
|
* used to submit the form).
|
|
* 3. This method binds the submit() method to the form for you.
|
|
*
|
|
* The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
|
|
* passes the options argument along after properly binding events for submit elements and
|
|
* the form itself.
|
|
*/
|
|
$.fn.ajaxForm = function(options) {
|
|
options = options || {};
|
|
options.delegation = options.delegation && $.isFunction($.fn.on);
|
|
|
|
// in jQuery 1.3+ we can fix mistakes with the ready state
|
|
if (!options.delegation && this.length === 0) {
|
|
var o = { s: this.selector, c: this.context };
|
|
if (!$.isReady && o.s) {
|
|
log('DOM not ready, queuing ajaxForm');
|
|
$(function() {
|
|
$(o.s,o.c).ajaxForm(options);
|
|
});
|
|
return this;
|
|
}
|
|
// is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
|
|
log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
|
|
return this;
|
|
}
|
|
|
|
if ( options.delegation ) {
|
|
$(document)
|
|
.off('submit.form-plugin', this.selector, doAjaxSubmit)
|
|
.off('click.form-plugin', this.selector, captureSubmittingElement)
|
|
.on('submit.form-plugin', this.selector, options, doAjaxSubmit)
|
|
.on('click.form-plugin', this.selector, options, captureSubmittingElement);
|
|
return this;
|
|
}
|
|
|
|
return this.ajaxFormUnbind()
|
|
.bind('submit.form-plugin', options, doAjaxSubmit)
|
|
.bind('click.form-plugin', options, captureSubmittingElement);
|
|
};
|
|
|
|
// private event handlers
|
|
function doAjaxSubmit(e) {
|
|
/*jshint validthis:true */
|
|
var options = e.data;
|
|
if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
|
|
e.preventDefault();
|
|
$(this).ajaxSubmit(options);
|
|
}
|
|
}
|
|
|
|
function captureSubmittingElement(e) {
|
|
/*jshint validthis:true */
|
|
var target = e.target;
|
|
var $el = $(target);
|
|
if (!($el.is("[type=submit],[type=image]"))) {
|
|
// is this a child element of the submit el? (ex: a span within a button)
|
|
var t = $el.closest('[type=submit]');
|
|
if (t.length === 0) {
|
|
return;
|
|
}
|
|
target = t[0];
|
|
}
|
|
var form = this;
|
|
form.clk = target;
|
|
if (target.type == 'image') {
|
|
if (e.offsetX !== undefined) {
|
|
form.clk_x = e.offsetX;
|
|
form.clk_y = e.offsetY;
|
|
} else if (typeof $.fn.offset == 'function') {
|
|
var offset = $el.offset();
|
|
form.clk_x = e.pageX - offset.left;
|
|
form.clk_y = e.pageY - offset.top;
|
|
} else {
|
|
form.clk_x = e.pageX - target.offsetLeft;
|
|
form.clk_y = e.pageY - target.offsetTop;
|
|
}
|
|
}
|
|
// clear form vars
|
|
setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
|
|
}
|
|
|
|
|
|
// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
|
|
$.fn.ajaxFormUnbind = function() {
|
|
return this.unbind('submit.form-plugin click.form-plugin');
|
|
};
|
|
|
|
/**
|
|
* formToArray() gathers form element data into an array of objects that can
|
|
* be passed to any of the following ajax functions: $.get, $.post, or load.
|
|
* Each object in the array has both a 'name' and 'value' property. An example of
|
|
* an array for a simple login form might be:
|
|
*
|
|
* [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
|
|
*
|
|
* It is this array that is passed to pre-submit callback functions provided to the
|
|
* ajaxSubmit() and ajaxForm() methods.
|
|
*/
|
|
$.fn.formToArray = function(semantic, elements) {
|
|
var a = [];
|
|
if (this.length === 0) {
|
|
return a;
|
|
}
|
|
|
|
var form = this[0];
|
|
var els = semantic ? form.getElementsByTagName('*') : form.elements;
|
|
if (!els) {
|
|
return a;
|
|
}
|
|
|
|
var i,j,n,v,el,max,jmax;
|
|
for(i=0, max=els.length; i < max; i++) {
|
|
el = els[i];
|
|
n = el.name;
|
|
if (!n) {
|
|
continue;
|
|
}
|
|
|
|
if (semantic && form.clk && el.type == "image") {
|
|
// handle image inputs on the fly when semantic == true
|
|
if(!el.disabled && form.clk == el) {
|
|
a.push({name: n, value: $(el).val(), type: el.type });
|
|
a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
v = $.fieldValue(el, true);
|
|
if (v && v.constructor == Array) {
|
|
if (elements)
|
|
elements.push(el);
|
|
for(j=0, jmax=v.length; j < jmax; j++) {
|
|
a.push({name: n, value: v[j]});
|
|
}
|
|
}
|
|
else if (feature.fileapi && el.type == 'file' && !el.disabled) {
|
|
if (elements)
|
|
elements.push(el);
|
|
var files = el.files;
|
|
if (files.length) {
|
|
for (j=0; j < files.length; j++) {
|
|
a.push({name: n, value: files[j], type: el.type});
|
|
}
|
|
}
|
|
else {
|
|
// #180
|
|
a.push({ name: n, value: '', type: el.type });
|
|
}
|
|
}
|
|
else if (v !== null && typeof v != 'undefined') {
|
|
if (elements)
|
|
elements.push(el);
|
|
a.push({name: n, value: v, type: el.type, required: el.required});
|
|
}
|
|
}
|
|
|
|
if (!semantic && form.clk) {
|
|
// input type=='image' are not found in elements array! handle it here
|
|
var $input = $(form.clk), input = $input[0];
|
|
n = input.name;
|
|
if (n && !input.disabled && input.type == 'image') {
|
|
a.push({name: n, value: $input.val()});
|
|
a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Serializes form data into a 'submittable' string. This method will return a string
|
|
* in the format: name1=value1&name2=value2
|
|
*/
|
|
$.fn.formSerialize = function(semantic) {
|
|
//hand off to jQuery.param for proper encoding
|
|
return $.param(this.formToArray(semantic));
|
|
};
|
|
|
|
/**
|
|
* Serializes all field elements in the jQuery object into a query string.
|
|
* This method will return a string in the format: name1=value1&name2=value2
|
|
*/
|
|
$.fn.fieldSerialize = function(successful) {
|
|
var a = [];
|
|
this.each(function() {
|
|
var n = this.name;
|
|
if (!n) {
|
|
return;
|
|
}
|
|
var v = $.fieldValue(this, successful);
|
|
if (v && v.constructor == Array) {
|
|
for (var i=0,max=v.length; i < max; i++) {
|
|
a.push({name: n, value: v[i]});
|
|
}
|
|
}
|
|
else if (v !== null && typeof v != 'undefined') {
|
|
a.push({name: this.name, value: v});
|
|
}
|
|
});
|
|
//hand off to jQuery.param for proper encoding
|
|
return $.param(a);
|
|
};
|
|
|
|
/**
|
|
* Returns the value(s) of the element in the matched set. For example, consider the following form:
|
|
*
|
|
* <form><fieldset>
|
|
* <input name="A" type="text" />
|
|
* <input name="A" type="text" />
|
|
* <input name="B" type="checkbox" value="B1" />
|
|
* <input name="B" type="checkbox" value="B2"/>
|
|
* <input name="C" type="radio" value="C1" />
|
|
* <input name="C" type="radio" value="C2" />
|
|
* </fieldset></form>
|
|
*
|
|
* var v = $('input[type=text]').fieldValue();
|
|
* // if no values are entered into the text inputs
|
|
* v == ['','']
|
|
* // if values entered into the text inputs are 'foo' and 'bar'
|
|
* v == ['foo','bar']
|
|
*
|
|
* var v = $('input[type=checkbox]').fieldValue();
|
|
* // if neither checkbox is checked
|
|
* v === undefined
|
|
* // if both checkboxes are checked
|
|
* v == ['B1', 'B2']
|
|
*
|
|
* var v = $('input[type=radio]').fieldValue();
|
|
* // if neither radio is checked
|
|
* v === undefined
|
|
* // if first radio is checked
|
|
* v == ['C1']
|
|
*
|
|
* The successful argument controls whether or not the field element must be 'successful'
|
|
* (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
|
|
* The default value of the successful argument is true. If this value is false the value(s)
|
|
* for each element is returned.
|
|
*
|
|
* Note: This method *always* returns an array. If no valid value can be determined the
|
|
* array will be empty, otherwise it will contain one or more values.
|
|
*/
|
|
$.fn.fieldValue = function(successful) {
|
|
for (var val=[], i=0, max=this.length; i < max; i++) {
|
|
var el = this[i];
|
|
var v = $.fieldValue(el, successful);
|
|
if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
|
|
continue;
|
|
}
|
|
if (v.constructor == Array)
|
|
$.merge(val, v);
|
|
else
|
|
val.push(v);
|
|
}
|
|
return val;
|
|
};
|
|
|
|
/**
|
|
* Returns the value of the field element.
|
|
*/
|
|
$.fieldValue = function(el, successful) {
|
|
var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
|
|
if (successful === undefined) {
|
|
successful = true;
|
|
}
|
|
|
|
if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
|
|
(t == 'checkbox' || t == 'radio') && !el.checked ||
|
|
(t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
|
|
tag == 'select' && el.selectedIndex == -1)) {
|
|
return null;
|
|
}
|
|
|
|
if (tag == 'select') {
|
|
var index = el.selectedIndex;
|
|
if (index < 0) {
|
|
return null;
|
|
}
|
|
var a = [], ops = el.options;
|
|
var one = (t == 'select-one');
|
|
var max = (one ? index+1 : ops.length);
|
|
for(var i=(one ? index : 0); i < max; i++) {
|
|
var op = ops[i];
|
|
if (op.selected) {
|
|
var v = op.value;
|
|
if (!v) { // extra pain for IE...
|
|
v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
|
|
}
|
|
if (one) {
|
|
return v;
|
|
}
|
|
a.push(v);
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
return $(el).val();
|
|
};
|
|
|
|
/**
|
|
* Clears the form data. Takes the following actions on the form's input fields:
|
|
* - input text fields will have their 'value' property set to the empty string
|
|
* - select elements will have their 'selectedIndex' property set to -1
|
|
* - checkbox and radio inputs will have their 'checked' property set to false
|
|
* - inputs of type submit, button, reset, and hidden will *not* be effected
|
|
* - button elements will *not* be effected
|
|
*/
|
|
$.fn.clearForm = function(includeHidden) {
|
|
return this.each(function() {
|
|
$('input,select,textarea', this).clearFields(includeHidden);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Clears the selected form elements.
|
|
*/
|
|
$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
|
|
var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
|
|
return this.each(function() {
|
|
var t = this.type, tag = this.tagName.toLowerCase();
|
|
if (re.test(t) || tag == 'textarea') {
|
|
this.value = '';
|
|
}
|
|
else if (t == 'checkbox' || t == 'radio') {
|
|
this.checked = false;
|
|
}
|
|
else if (tag == 'select') {
|
|
this.selectedIndex = -1;
|
|
}
|
|
else if (t == "file") {
|
|
if (/MSIE/.test(navigator.userAgent)) {
|
|
$(this).replaceWith($(this).clone());
|
|
} else {
|
|
$(this).val('');
|
|
}
|
|
}
|
|
else if (includeHidden) {
|
|
// includeHidden can be the value true, or it can be a selector string
|
|
// indicating a special test; for example:
|
|
// $('#myForm').clearForm('.special:hidden')
|
|
// the above would clean hidden inputs that have the class of 'special'
|
|
if ( (includeHidden === true && /hidden/.test(t)) ||
|
|
(typeof includeHidden == 'string' && $(this).is(includeHidden)) )
|
|
this.value = '';
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Resets the form data. Causes all form elements to be reset to their original value.
|
|
*/
|
|
$.fn.resetForm = function() {
|
|
return this.each(function() {
|
|
// guard against an input with the name of 'reset'
|
|
// note that IE reports the reset function as an 'object'
|
|
if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
|
|
this.reset();
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Enables or disables any matching elements.
|
|
*/
|
|
$.fn.enable = function(b) {
|
|
if (b === undefined) {
|
|
b = true;
|
|
}
|
|
return this.each(function() {
|
|
this.disabled = !b;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks/unchecks any matching checkboxes or radio buttons and
|
|
* selects/deselects and matching option elements.
|
|
*/
|
|
$.fn.selected = function(select) {
|
|
if (select === undefined) {
|
|
select = true;
|
|
}
|
|
return this.each(function() {
|
|
var t = this.type;
|
|
if (t == 'checkbox' || t == 'radio') {
|
|
this.checked = select;
|
|
}
|
|
else if (this.tagName.toLowerCase() == 'option') {
|
|
var $sel = $(this).parent('select');
|
|
if (select && $sel[0] && $sel[0].type == 'select-one') {
|
|
// deselect all other options
|
|
$sel.find('option').selected(false);
|
|
}
|
|
this.selected = select;
|
|
}
|
|
});
|
|
};
|
|
|
|
// expose debug var
|
|
$.fn.ajaxSubmit.debug = false;
|
|
|
|
// helper fn for console logging
|
|
function log() {
|
|
if (!$.fn.ajaxSubmit.debug)
|
|
return;
|
|
var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
|
|
if (window.console && window.console.log) {
|
|
window.console.log(msg);
|
|
}
|
|
else if (window.opera && window.opera.postError) {
|
|
window.opera.postError(msg);
|
|
}
|
|
}
|
|
|
|
})(jQuery);
|
|
|
|
/**
|
|
* jQuery.timers - Timer abstractions for jQuery
|
|
* Written by Blair Mitchelmore (blair DOT mitchelmore AT gmail DOT com)
|
|
* Licensed under the WTFPL (http://sam.zoy.org/wtfpl/).
|
|
* Date: 2009/10/16
|
|
*
|
|
* @author Blair Mitchelmore
|
|
* @version 1.2
|
|
*
|
|
**/
|
|
|
|
jQuery.fn.extend({
|
|
everyTime: function(interval, label, fn, times) {
|
|
return this.each(function() {
|
|
jQuery.timer.add(this, interval, label, fn, times);
|
|
});
|
|
},
|
|
oneTime: function(interval, label, fn) {
|
|
return this.each(function() {
|
|
jQuery.timer.add(this, interval, label, fn, 1);
|
|
});
|
|
},
|
|
stopTime: function(label, fn) {
|
|
return this.each(function() {
|
|
jQuery.timer.remove(this, label, fn);
|
|
});
|
|
}
|
|
});
|
|
|
|
jQuery.extend({
|
|
timer: {
|
|
global: [],
|
|
guid: 1,
|
|
dataKey: "jQuery.timer",
|
|
regex: /^([0-9]+(?:\.[0-9]*)?)\s*(.*s)?$/,
|
|
powers: {
|
|
// Yeah this is major overkill...
|
|
'ms': 1,
|
|
'cs': 10,
|
|
'ds': 100,
|
|
's': 1000,
|
|
'das': 10000,
|
|
'hs': 100000,
|
|
'ks': 1000000
|
|
},
|
|
timeParse: function(value) {
|
|
if (value == undefined || value == null)
|
|
return null;
|
|
var result = this.regex.exec(jQuery.trim(value.toString()));
|
|
if (result[2]) {
|
|
var num = parseFloat(result[1]);
|
|
var mult = this.powers[result[2]] || 1;
|
|
return num * mult;
|
|
} else {
|
|
return value;
|
|
}
|
|
},
|
|
add: function(element, interval, label, fn, times) {
|
|
var counter = 0;
|
|
|
|
if (jQuery.isFunction(label)) {
|
|
if (!times)
|
|
times = fn;
|
|
fn = label;
|
|
label = interval;
|
|
}
|
|
|
|
interval = jQuery.timer.timeParse(interval);
|
|
|
|
if (typeof interval != 'number' || isNaN(interval) || interval < 0)
|
|
return;
|
|
|
|
if (typeof times != 'number' || isNaN(times) || times < 0)
|
|
times = 0;
|
|
|
|
times = times || 0;
|
|
|
|
var timers = jQuery.data(element, this.dataKey) || jQuery.data(element, this.dataKey, {});
|
|
|
|
if (!timers[label])
|
|
timers[label] = {};
|
|
|
|
fn.timerID = fn.timerID || this.guid++;
|
|
|
|
var handler = function() {
|
|
if ((++counter > times && times !== 0) || fn.call(element, counter) === false)
|
|
jQuery.timer.remove(element, label, fn);
|
|
};
|
|
|
|
handler.timerID = fn.timerID;
|
|
|
|
if (!timers[label][fn.timerID])
|
|
timers[label][fn.timerID] = window.setInterval(handler,interval);
|
|
|
|
this.global.push( element );
|
|
|
|
},
|
|
remove: function(element, label, fn) {
|
|
var timers = jQuery.data(element, this.dataKey), ret;
|
|
|
|
if ( timers ) {
|
|
|
|
if (!label) {
|
|
for ( label in timers )
|
|
this.remove(element, label, fn);
|
|
} else if ( timers[label] ) {
|
|
if ( fn ) {
|
|
if ( fn.timerID ) {
|
|
window.clearInterval(timers[label][fn.timerID]);
|
|
delete timers[label][fn.timerID];
|
|
}
|
|
} else {
|
|
for ( var fn in timers[label] ) {
|
|
window.clearInterval(timers[label][fn]);
|
|
delete timers[label][fn];
|
|
}
|
|
}
|
|
|
|
for ( ret in timers[label] ) break;
|
|
if ( !ret ) {
|
|
ret = null;
|
|
delete timers[label];
|
|
}
|
|
}
|
|
|
|
for ( ret in timers ) break;
|
|
if ( !ret )
|
|
jQuery.removeData(element, this.dataKey);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
jQuery(window).bind("unload", function() {
|
|
jQuery.each(jQuery.timer.global, function(index, item) {
|
|
jQuery.timer.remove(item);
|
|
});
|
|
});
|
|
|
|
/*! http://mths.be/placeholder v2.0.7 by @mathias */
|
|
;(function(window, document, $) {
|
|
|
|
var isInputSupported = 'placeholder' in document.createElement('input'),
|
|
isTextareaSupported = 'placeholder' in document.createElement('textarea'),
|
|
prototype = $.fn,
|
|
valHooks = $.valHooks,
|
|
hooks,
|
|
placeholder;
|
|
|
|
if (isInputSupported && isTextareaSupported) {
|
|
|
|
placeholder = prototype.placeholder = function() {
|
|
return this;
|
|
};
|
|
|
|
placeholder.input = placeholder.textarea = true;
|
|
|
|
} else {
|
|
|
|
placeholder = prototype.placeholder = function() {
|
|
var $this = this;
|
|
$this
|
|
.filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
|
|
.not('.placeholder')
|
|
.bind({
|
|
'focus.placeholder': clearPlaceholder,
|
|
'blur.placeholder': setPlaceholder
|
|
})
|
|
.data('placeholder-enabled', true)
|
|
.trigger('blur.placeholder');
|
|
return $this;
|
|
};
|
|
|
|
placeholder.input = isInputSupported;
|
|
placeholder.textarea = isTextareaSupported;
|
|
|
|
hooks = {
|
|
'get': function(element) {
|
|
var $element = $(element);
|
|
return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
|
|
},
|
|
'set': function(element, value) {
|
|
var $element = $(element);
|
|
if (!$element.data('placeholder-enabled')) {
|
|
return element.value = value;
|
|
}
|
|
if (value == '') {
|
|
element.value = value;
|
|
// Issue #56: Setting the placeholder causes problems if the element continues to have focus.
|
|
if (element != document.activeElement) {
|
|
// We can't use `triggerHandler` here because of dummy text/password inputs :(
|
|
setPlaceholder.call(element);
|
|
}
|
|
} else if ($element.hasClass('placeholder')) {
|
|
clearPlaceholder.call(element, true, value) || (element.value = value);
|
|
} else {
|
|
element.value = value;
|
|
}
|
|
// `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
|
|
return $element;
|
|
}
|
|
};
|
|
|
|
isInputSupported || (valHooks.input = hooks);
|
|
isTextareaSupported || (valHooks.textarea = hooks);
|
|
|
|
$(function() {
|
|
// Look for forms
|
|
$(document).delegate('form', 'submit.placeholder', function() {
|
|
// Clear the placeholder values so they don't get submitted
|
|
var $inputs = $('.placeholder', this).each(clearPlaceholder);
|
|
setTimeout(function() {
|
|
$inputs.each(setPlaceholder);
|
|
}, 10);
|
|
});
|
|
});
|
|
|
|
// Clear placeholder values upon page reload
|
|
$(window).bind('beforeunload.placeholder', function() {
|
|
$('.placeholder').each(function() {
|
|
this.value = '';
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
function args(elem) {
|
|
// Return an object of element attributes
|
|
var newAttrs = {},
|
|
rinlinejQuery = /^jQuery\d+$/;
|
|
$.each(elem.attributes, function(i, attr) {
|
|
if (attr.specified && !rinlinejQuery.test(attr.name)) {
|
|
newAttrs[attr.name] = attr.value;
|
|
}
|
|
});
|
|
return newAttrs;
|
|
}
|
|
|
|
function clearPlaceholder(event, value) {
|
|
var input = this,
|
|
$input = $(input);
|
|
if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) {
|
|
if ($input.data('placeholder-password')) {
|
|
$input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
|
|
// If `clearPlaceholder` was called from `$.valHooks.input.set`
|
|
if (event === true) {
|
|
return $input[0].value = value;
|
|
}
|
|
$input.focus();
|
|
} else {
|
|
input.value = '';
|
|
$input.removeClass('placeholder');
|
|
input == document.activeElement && input.select();
|
|
}
|
|
}
|
|
}
|
|
|
|
function setPlaceholder() {
|
|
var $replacement,
|
|
input = this,
|
|
$input = $(input),
|
|
$origInput = $input,
|
|
id = this.id;
|
|
if (input.value == '') {
|
|
if (input.type == 'password') {
|
|
if (!$input.data('placeholder-textinput')) {
|
|
try {
|
|
$replacement = $input.clone().attr({ 'type': 'text' });
|
|
} catch(e) {
|
|
$replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
|
|
}
|
|
$replacement
|
|
.removeAttr('name')
|
|
.data({
|
|
'placeholder-password': true,
|
|
'placeholder-id': id
|
|
})
|
|
.bind('focus.placeholder', clearPlaceholder);
|
|
$input
|
|
.data({
|
|
'placeholder-textinput': $replacement,
|
|
'placeholder-id': id
|
|
})
|
|
.before($replacement);
|
|
}
|
|
$input = $input.removeAttr('id').hide().prev().attr('id', id).show();
|
|
// Note: `$input[0] != input` now!
|
|
}
|
|
$input.addClass('placeholder');
|
|
$input[0].value = $input.attr('placeholder');
|
|
} else {
|
|
$input.removeClass('placeholder');
|
|
}
|
|
}
|
|
|
|
}(this, document, jQuery));
|
|
$.fn.selectRange = function(start, end) {
|
|
return this.each(function() {
|
|
if (this.setSelectionRange) {
|
|
this.focus();
|
|
this.setSelectionRange(start, end);
|
|
} else if (this.createTextRange) {
|
|
var range = this.createTextRange();
|
|
range.collapse(true);
|
|
range.moveEnd('character', end);
|
|
range.moveStart('character', start);
|
|
range.select();
|
|
}
|
|
});
|
|
};
|
|
|
|
// License: PD
|
|
|
|
// This code was written by Tyler Akins and has been placed in the
|
|
// public domain. It would be nice if you left this header intact.
|
|
// Base64 code from Tyler Akins -- http://rumkin.com
|
|
|
|
var Base64 = (function () {
|
|
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
|
|
var obj = {
|
|
/**
|
|
* Encodes a string in base64
|
|
* @param {String} input The string to encode in base64.
|
|
*/
|
|
encode: function (input) {
|
|
var output = "";
|
|
var chr1, chr2, chr3;
|
|
var enc1, enc2, enc3, enc4;
|
|
var i = 0;
|
|
|
|
do {
|
|
chr1 = input.charCodeAt(i++);
|
|
chr2 = input.charCodeAt(i++);
|
|
chr3 = input.charCodeAt(i++);
|
|
|
|
enc1 = chr1 >> 2;
|
|
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
|
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
|
enc4 = chr3 & 63;
|
|
|
|
if (isNaN(chr2)) {
|
|
enc3 = enc4 = 64;
|
|
} else if (isNaN(chr3)) {
|
|
enc4 = 64;
|
|
}
|
|
|
|
output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
|
|
keyStr.charAt(enc3) + keyStr.charAt(enc4);
|
|
} while (i < input.length);
|
|
|
|
return output;
|
|
},
|
|
|
|
/**
|
|
* Decodes a base64 string.
|
|
* @param {String} input The string to decode.
|
|
*/
|
|
decode: function (input) {
|
|
var output = "";
|
|
var chr1, chr2, chr3;
|
|
var enc1, enc2, enc3, enc4;
|
|
var i = 0;
|
|
|
|
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
|
|
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
|
|
|
do {
|
|
enc1 = keyStr.indexOf(input.charAt(i++));
|
|
enc2 = keyStr.indexOf(input.charAt(i++));
|
|
enc3 = keyStr.indexOf(input.charAt(i++));
|
|
enc4 = keyStr.indexOf(input.charAt(i++));
|
|
|
|
chr1 = (enc1 << 2) | (enc2 >> 4);
|
|
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
|
chr3 = ((enc3 & 3) << 6) | enc4;
|
|
|
|
output = output + String.fromCharCode(chr1);
|
|
|
|
if (enc3 != 64) {
|
|
output = output + String.fromCharCode(chr2);
|
|
}
|
|
if (enc4 != 64) {
|
|
output = output + String.fromCharCode(chr3);
|
|
}
|
|
} while (i < input.length);
|
|
|
|
return output;
|
|
}
|
|
};
|
|
|
|
return obj;
|
|
})();
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
This is the JSJaC library for Jappix (from trunk)
|
|
|
|
-------------------------------------------------
|
|
|
|
Licenses: Mozilla Public License version 1.1, GNU GPL, AGPL
|
|
Authors: Stefan Strigler, Valérian Saliou, Zash, Maranda
|
|
Last revision: 07/07/13
|
|
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Magic dependency loading. Taken from script.aculo.us
|
|
* and modified to break it.
|
|
* @author Stefan Strigler steve@zeank.in-berlin.de
|
|
* @version $Revision$
|
|
*/
|
|
|
|
var JSJaC = {
|
|
Version: '$Rev$',
|
|
bind: function(fn, obj, optArg) {
|
|
return function(arg) {
|
|
return fn.apply(obj, [arg, optArg]);
|
|
};
|
|
}
|
|
};
|
|
|
|
if (typeof JSJaCConnection == 'undefined')
|
|
JSJaC.load();
|
|
|
|
|
|
|
|
/* Copyright 2006 Erik Arvidsson
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
* may not use this file except in compliance with the License. You
|
|
* may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
* implied. See the License for the specific language governing
|
|
* permissions and limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Wrapper to make working with XmlHttpRequest and the
|
|
* DOM more convenient (cross browser compliance).
|
|
* this code is taken from
|
|
* http://webfx.eae.net/dhtml/xmlextras/xmlextras.html
|
|
* @author Stefan Strigler steve@zeank.in-berlin.de
|
|
* @version $Revision$
|
|
*/
|
|
|
|
/**
|
|
* XmlHttp factory
|
|
* @private
|
|
*/
|
|
function XmlHttp() {}
|
|
|
|
/**
|
|
* creates a cross browser compliant XmlHttpRequest object
|
|
*/
|
|
XmlHttp.create = function () {
|
|
try {
|
|
// Are we cross-domain?
|
|
if(!BOSH_SAME_ORIGIN) {
|
|
// Able to use CORS?
|
|
if (window.XMLHttpRequest) {
|
|
var req = new XMLHttpRequest();
|
|
|
|
if (req.withCredentials !== undefined)
|
|
return req;
|
|
}
|
|
|
|
// Fallback on JSONP
|
|
return new jXHR();
|
|
}
|
|
// Might be local-domain?
|
|
if (window.XMLHttpRequest) {
|
|
var req = new XMLHttpRequest();
|
|
|
|
// some versions of Moz do not support the readyState property
|
|
// and the onreadystate event so we patch it!
|
|
if (req.readyState == null) {
|
|
req.readyState = 1;
|
|
req.addEventListener("load", function () {
|
|
req.readyState = 4;
|
|
if (typeof req.onreadystatechange == "function")
|
|
req.onreadystatechange();
|
|
}, false);
|
|
}
|
|
|
|
return req;
|
|
}
|
|
if (window.ActiveXObject) {
|
|
return new ActiveXObject(XmlHttp.getPrefix() + ".XmlHttp");
|
|
}
|
|
}
|
|
catch (ex) {}
|
|
// fell through
|
|
throw new Error("Your browser does not support XmlHttp objects");
|
|
};
|
|
|
|
/**
|
|
* used to find the Automation server name
|
|
* @private
|
|
*/
|
|
XmlHttp.getPrefix = function() {
|
|
if (XmlHttp.prefix) // I know what you did last summer
|
|
return XmlHttp.prefix;
|
|
|
|
var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
|
|
var o;
|
|
for (var i = 0; i < prefixes.length; i++) {
|
|
try {
|
|
// try to create the objects
|
|
o = new ActiveXObject(prefixes[i] + ".XmlHttp");
|
|
return XmlHttp.prefix = prefixes[i];
|
|
}
|
|
catch (ex) {};
|
|
}
|
|
|
|
throw new Error("Could not find an installed XML parser");
|
|
};
|
|
|
|
|
|
/**
|
|
* XmlDocument factory
|
|
* @private
|
|
*/
|
|
function XmlDocument() {}
|
|
|
|
XmlDocument.create = function (name,ns) {
|
|
name = name || 'foo';
|
|
ns = ns || '';
|
|
|
|
try {
|
|
var doc;
|
|
// DOM2
|
|
if (document.implementation && document.implementation.createDocument) {
|
|
doc = document.implementation.createDocument(ns, name, null);
|
|
// some versions of Moz do not support the readyState property
|
|
// and the onreadystate event so we patch it!
|
|
if (doc.readyState == null) {
|
|
doc.readyState = 1;
|
|
doc.addEventListener("load", function () {
|
|
doc.readyState = 4;
|
|
if (typeof doc.onreadystatechange == "function")
|
|
doc.onreadystatechange();
|
|
}, false);
|
|
}
|
|
} else if (window.ActiveXObject) {
|
|
doc = new ActiveXObject(XmlDocument.getPrefix() + ".DomDocument");
|
|
}
|
|
|
|
if (!doc.documentElement || doc.documentElement.tagName != name ||
|
|
(doc.documentElement.namespaceURI &&
|
|
doc.documentElement.namespaceURI != ns)) {
|
|
try {
|
|
if (ns != '')
|
|
doc.appendChild(doc.createElement(name)).
|
|
setAttribute('xmlns',ns);
|
|
else
|
|
doc.appendChild(doc.createElement(name));
|
|
} catch (dex) {
|
|
doc = document.implementation.createDocument(ns,name,null);
|
|
|
|
if (doc.documentElement == null)
|
|
doc.appendChild(doc.createElement(name));
|
|
|
|
// fix buggy opera 8.5x
|
|
if (ns != '' &&
|
|
doc.documentElement.getAttribute('xmlns') != ns) {
|
|
doc.documentElement.setAttribute('xmlns',ns);
|
|
}
|
|
}
|
|
}
|
|
|
|
return doc;
|
|
}
|
|
catch (ex) { }
|
|
throw new Error("Your browser does not support XmlDocument objects");
|
|
};
|
|
|
|
/**
|
|
* used to find the Automation server name
|
|
* @private
|
|
*/
|
|
XmlDocument.getPrefix = function() {
|
|
if (XmlDocument.prefix)
|
|
return XmlDocument.prefix;
|
|
|
|
var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
|
|
var o;
|
|
for (var i = 0; i < prefixes.length; i++) {
|
|
try {
|
|
// try to create the objects
|
|
o = new ActiveXObject(prefixes[i] + ".DomDocument");
|
|
return XmlDocument.prefix = prefixes[i];
|
|
}
|
|
catch (ex) {};
|
|
}
|
|
|
|
throw new Error("Could not find an installed XML parser");
|
|
};
|
|
|
|
|
|
// Create the loadXML method
|
|
if (typeof(Document) != 'undefined' && window.DOMParser) {
|
|
|
|
/**
|
|
* XMLDocument did not extend the Document interface in some
|
|
* versions of Mozilla.
|
|
* @private
|
|
*/
|
|
Document.prototype.loadXML = function (s) {
|
|
|
|
// parse the string to a new doc
|
|
var doc2 = (new DOMParser()).parseFromString(s, "text/xml");
|
|
|
|
// remove all initial children
|
|
while (this.hasChildNodes())
|
|
this.removeChild(this.lastChild);
|
|
|
|
// insert and import nodes
|
|
for (var i = 0; i < doc2.childNodes.length; i++) {
|
|
this.appendChild(this.importNode(doc2.childNodes[i], true));
|
|
}
|
|
};
|
|
}
|
|
|
|
// Create xml getter for Mozilla
|
|
if (window.XMLSerializer &&
|
|
window.Node && Node.prototype && Node.prototype.__defineGetter__) {
|
|
|
|
/**
|
|
* xml getter
|
|
*
|
|
* This serializes the DOM tree to an XML String
|
|
*
|
|
* Usage: var sXml = oNode.xml
|
|
* @deprecated
|
|
* @private
|
|
*/
|
|
// XMLDocument did not extend the Document interface in some versions
|
|
// of Mozilla. Extend both!
|
|
XMLDocument.prototype.__defineGetter__("xml", function () {
|
|
return (new XMLSerializer()).serializeToString(this);
|
|
});
|
|
/**
|
|
* xml getter
|
|
*
|
|
* This serializes the DOM tree to an XML String
|
|
*
|
|
* Usage: var sXml = oNode.xml
|
|
* @deprecated
|
|
* @private
|
|
*/
|
|
Document.prototype.__defineGetter__("xml", function () {
|
|
return (new XMLSerializer()).serializeToString(this);
|
|
});
|
|
|
|
/**
|
|
* xml getter
|
|
*
|
|
* This serializes the DOM tree to an XML String
|
|
*
|
|
* Usage: var sXml = oNode.xml
|
|
* @deprecated
|
|
* @private
|
|
*/
|
|
Node.prototype.__defineGetter__("xml", function () {
|
|
return (new XMLSerializer()).serializeToString(this);
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* @fileoverview Collection of functions to make live easier
|
|
* @author Stefan Strigler
|
|
* @version $Revision$
|
|
*/
|
|
|
|
/**
|
|
* Convert special chars to HTML entities
|
|
* @addon
|
|
* @return The string with chars encoded for HTML
|
|
* @type String
|
|
*/
|
|
String.prototype.htmlEnc = function() {
|
|
if(!this)
|
|
return this;
|
|
|
|
return this.replace(/&(?!(amp|apos|gt|lt|quot);)/g,"&")
|
|
.replace(/</g,"<")
|
|
.replace(/>/g,">")
|
|
.replace(/\'/g,"'")
|
|
.replace(/\"/g,""")
|
|
.replace(/\n/g,"<br />");
|
|
};
|
|
|
|
/**
|
|
* Convert HTML entities to special chars
|
|
* @addon
|
|
* @return The normal string
|
|
* @type String
|
|
*/
|
|
String.prototype.revertHtmlEnc = function() {
|
|
if(!this)
|
|
return this;
|
|
|
|
var str = this.replace(/&/gi,'&');
|
|
str = str.replace(/</gi,'<');
|
|
str = str.replace(/>/gi,'>');
|
|
str = str.replace(/'/gi,'\'');
|
|
str = str.replace(/"/gi,'\"');
|
|
str = str.replace(/<br( )?(\/)?>/gi,'\n');
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* Converts from jabber timestamps to JavaScript Date objects
|
|
* @addon
|
|
* @param {String} ts A string representing a jabber datetime timestamp as
|
|
* defined by {@link http://www.xmpp.org/extensions/xep-0082.html XEP-0082}
|
|
* @return A javascript Date object corresponding to the jabber DateTime given
|
|
* @type Date
|
|
*/
|
|
Date.jab2date = function(ts) {
|
|
// Timestamp
|
|
if(!isNaN(ts))
|
|
return new Date(ts * 1000);
|
|
|
|
// Get the UTC date
|
|
var date = new Date(Date.UTC(ts.substr(0,4),ts.substr(5,2)-1,ts.substr(8,2),ts.substr(11,2),ts.substr(14,2),ts.substr(17,2)));
|
|
|
|
if (ts.substr(ts.length-6,1) != 'Z') { // there's an offset
|
|
var date_offset = date.getTimezoneOffset() * 60 * 1000;
|
|
var offset = new Date();
|
|
offset.setTime(0);
|
|
offset.setUTCHours(ts.substr(ts.length-5,2));
|
|
offset.setUTCMinutes(ts.substr(ts.length-2,2));
|
|
if (ts.substr(ts.length-6,1) == '+')
|
|
date.setTime(date.getTime() + offset.getTime() + date_offset);
|
|
else if (ts.substr(ts.length-6,1) == '-')
|
|
date.setTime(date.getTime() - offset.getTime() + date_offset);
|
|
}
|
|
return date;
|
|
};
|
|
|
|
/**
|
|
* Takes a timestamp in the form of 2004-08-13T12:07:04+02:00 as argument
|
|
* and converts it to some sort of humane readable format
|
|
* @addon
|
|
*/
|
|
Date.hrTime = function(ts) {
|
|
return Date.jab2date(ts).toLocaleString();
|
|
};
|
|
|
|
/**
|
|
* somewhat opposit to {@link #hrTime}
|
|
* expects a javascript Date object as parameter and returns a jabber
|
|
* date string conforming to
|
|
* {@link http://www.xmpp.org/extensions/xep-0082.html XEP-0082}
|
|
* @see #hrTime
|
|
* @return The corresponding jabber DateTime string
|
|
* @type String
|
|
*/
|
|
Date.prototype.jabberDate = function() {
|
|
var padZero = function(i) {
|
|
if (i < 10) return "0" + i;
|
|
return i;
|
|
};
|
|
|
|
var jDate = this.getUTCFullYear() + "-";
|
|
jDate += padZero(this.getUTCMonth()+1) + "-";
|
|
jDate += padZero(this.getUTCDate()) + "T";
|
|
jDate += padZero(this.getUTCHours()) + ":";
|
|
jDate += padZero(this.getUTCMinutes()) + ":";
|
|
jDate += padZero(this.getUTCSeconds()) + "Z";
|
|
|
|
return jDate;
|
|
};
|
|
|
|
/**
|
|
* Determines the maximum of two given numbers
|
|
* @addon
|
|
* @param {Number} A a number
|
|
* @param {Number} B another number
|
|
* @return the maximum of A and B
|
|
* @type Number
|
|
*/
|
|
Number.max = function(A, B) {
|
|
return (A > B)? A : B;
|
|
};
|
|
|
|
Number.min = function(A, B) {
|
|
return (A < B)? A : B;
|
|
};
|
|
|
|
|
|
/* Copyright (c) 1998 - 2007, Paul Johnston & Contributors
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following
|
|
* disclaimer. Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following
|
|
* disclaimer in the documentation and/or other materials provided
|
|
* with the distribution.
|
|
*
|
|
* Neither the name of the author nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this
|
|
* software without specific prior written permission.
|
|
*
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
|
* OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Collection of MD5 and SHA1 hashing and encoding
|
|
* methods.
|
|
* @author Stefan Strigler steve@zeank.in-berlin.de
|
|
*/
|
|
|
|
|
|
/*
|
|
* A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
|
|
* in FIPS PUB 180-1
|
|
* Version 2.1a Copyright Paul Johnston 2000 - 2002.
|
|
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
|
* Distributed under the BSD License
|
|
* See http://pajhome.org.uk/crypt/md5 for details.
|
|
*/
|
|
|
|
/*
|
|
* Configurable variables. You may need to tweak these to be compatible with
|
|
* the server-side, but the defaults work in most cases.
|
|
*/
|
|
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
|
|
var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */
|
|
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
|
|
|
|
/*
|
|
* These are the functions you'll usually want to call
|
|
* They take string arguments and return either hex or base-64 encoded strings
|
|
*/
|
|
function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
|
|
function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
|
|
function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
|
|
function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
|
|
function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
|
|
function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
|
|
|
|
/*
|
|
* Perform a simple self-test to see if the VM is working
|
|
*/
|
|
function sha1_vm_test()
|
|
{
|
|
return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
|
|
}
|
|
|
|
/*
|
|
* Calculate the SHA-1 of an array of big-endian words, and a bit length
|
|
*/
|
|
function core_sha1(x, len)
|
|
{
|
|
/* append padding */
|
|
x[len >> 5] |= 0x80 << (24 - len % 32);
|
|
x[((len + 64 >> 9) << 4) + 15] = len;
|
|
|
|
var w = new Array(80);
|
|
var a = 1732584193;
|
|
var b = -271733879;
|
|
var c = -1732584194;
|
|
var d = 271733878;
|
|
var e = -1009589776;
|
|
|
|
var i, j, t, olda, oldb, oldc, oldd, olde;
|
|
for (i = 0; i < x.length; i += 16)
|
|
{
|
|
olda = a;
|
|
oldb = b;
|
|
oldc = c;
|
|
oldd = d;
|
|
olde = e;
|
|
|
|
for (j = 0; j < 80; j++)
|
|
{
|
|
if (j < 16) { w[j] = x[i + j]; }
|
|
else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); }
|
|
t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
|
|
safe_add(safe_add(e, w[j]), sha1_kt(j)));
|
|
e = d;
|
|
d = c;
|
|
c = rol(b, 30);
|
|
b = a;
|
|
a = t;
|
|
}
|
|
|
|
a = safe_add(a, olda);
|
|
b = safe_add(b, oldb);
|
|
c = safe_add(c, oldc);
|
|
d = safe_add(d, oldd);
|
|
e = safe_add(e, olde);
|
|
}
|
|
return [a, b, c, d, e];
|
|
}
|
|
|
|
/*
|
|
* Perform the appropriate triplet combination function for the current
|
|
* iteration
|
|
*/
|
|
function sha1_ft(t, b, c, d)
|
|
{
|
|
if (t < 20) { return (b & c) | ((~b) & d); }
|
|
if (t < 40) { return b ^ c ^ d; }
|
|
if (t < 60) { return (b & c) | (b & d) | (c & d); }
|
|
return b ^ c ^ d;
|
|
}
|
|
|
|
/*
|
|
* Determine the appropriate additive constant for the current iteration
|
|
*/
|
|
function sha1_kt(t)
|
|
{
|
|
return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
|
|
(t < 60) ? -1894007588 : -899497514;
|
|
}
|
|
|
|
/*
|
|
* Calculate the HMAC-SHA1 of a key and some data
|
|
*/
|
|
function core_hmac_sha1(key, data)
|
|
{
|
|
var bkey = str2binb(key);
|
|
if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * chrsz); }
|
|
|
|
var ipad = new Array(16), opad = new Array(16);
|
|
for (var i = 0; i < 16; i++)
|
|
{
|
|
ipad[i] = bkey[i] ^ 0x36363636;
|
|
opad[i] = bkey[i] ^ 0x5C5C5C5C;
|
|
}
|
|
|
|
var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
|
|
return core_sha1(opad.concat(hash), 512 + 160);
|
|
}
|
|
|
|
/*
|
|
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
|
* to work around bugs in some JS interpreters.
|
|
*/
|
|
function safe_add(x, y)
|
|
{
|
|
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
|
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
|
return (msw << 16) | (lsw & 0xFFFF);
|
|
}
|
|
|
|
/*
|
|
* Bitwise rotate a 32-bit number to the left.
|
|
*/
|
|
function rol(num, cnt)
|
|
{
|
|
return (num << cnt) | (num >>> (32 - cnt));
|
|
}
|
|
|
|
/*
|
|
* Convert an 8-bit or 16-bit string to an array of big-endian words
|
|
* In 8-bit function, characters >255 have their hi-byte silently ignored.
|
|
*/
|
|
function str2binb(str)
|
|
{
|
|
var bin = [];
|
|
var mask = (1 << chrsz) - 1;
|
|
for (var i = 0; i < str.length * chrsz; i += chrsz)
|
|
{
|
|
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
|
|
}
|
|
return bin;
|
|
}
|
|
|
|
/*
|
|
* Convert an array of big-endian words to a string
|
|
*/
|
|
function binb2str(bin)
|
|
{
|
|
var str = "";
|
|
var mask = (1 << chrsz) - 1;
|
|
for (var i = 0; i < bin.length * 32; i += chrsz)
|
|
{
|
|
str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/*
|
|
* Convert an array of big-endian words to a hex string.
|
|
*/
|
|
function binb2hex(binarray)
|
|
{
|
|
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
|
|
var str = "";
|
|
for (var i = 0; i < binarray.length * 4; i++)
|
|
{
|
|
str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
|
|
hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/*
|
|
* Convert an array of big-endian words to a base-64 string
|
|
*/
|
|
function binb2b64(binarray)
|
|
{
|
|
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
var str = "";
|
|
var triplet, j;
|
|
for (var i = 0; i < binarray.length * 4; i += 3)
|
|
{
|
|
triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) |
|
|
(((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) |
|
|
((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
|
|
for (j = 0; j < 4; j++)
|
|
{
|
|
if (i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
|
|
else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
|
|
}
|
|
}
|
|
return str;
|
|
}
|
|
|
|
|
|
/*
|
|
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
|
* Digest Algorithm, as defined in RFC 1321.
|
|
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
|
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
|
* Distributed under the BSD License
|
|
* See http://pajhome.org.uk/crypt/md5 for more info.
|
|
*/
|
|
|
|
/*
|
|
* These are the functions you'll usually want to call
|
|
* They take string arguments and return either hex or base-64 encoded strings
|
|
*/
|
|
function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
|
|
function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
|
|
function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
|
|
function hex_hmac_md5(k, d)
|
|
{ return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
|
|
function b64_hmac_md5(k, d)
|
|
{ return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
|
|
function any_hmac_md5(k, d, e)
|
|
{ return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
|
|
|
|
/*
|
|
* Perform a simple self-test to see if the VM is working
|
|
*/
|
|
function md5_vm_test()
|
|
{
|
|
return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
|
|
}
|
|
|
|
/*
|
|
* Calculate the MD5 of a raw string
|
|
*/
|
|
function rstr_md5(s)
|
|
{
|
|
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
|
|
}
|
|
|
|
/*
|
|
* Calculate the HMAC-MD5, of a key and some data (raw strings)
|
|
*/
|
|
function rstr_hmac_md5(key, data)
|
|
{
|
|
var bkey = rstr2binl(key);
|
|
if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
|
|
|
|
var ipad = Array(16), opad = Array(16);
|
|
for(var i = 0; i < 16; i++)
|
|
{
|
|
ipad[i] = bkey[i] ^ 0x36363636;
|
|
opad[i] = bkey[i] ^ 0x5C5C5C5C;
|
|
}
|
|
|
|
var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
|
|
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
|
|
}
|
|
|
|
/*
|
|
* Convert a raw string to a hex string
|
|
*/
|
|
function rstr2hex(input)
|
|
{
|
|
try { hexcase } catch(e) { hexcase=0; }
|
|
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
|
|
var output = "";
|
|
var x;
|
|
for(var i = 0; i < input.length; i++)
|
|
{
|
|
x = input.charCodeAt(i);
|
|
output += hex_tab.charAt((x >>> 4) & 0x0F)
|
|
+ hex_tab.charAt( x & 0x0F);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Convert a raw string to a base-64 string
|
|
*/
|
|
function rstr2b64(input)
|
|
{
|
|
try { b64pad } catch(e) { b64pad=''; }
|
|
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
var output = "";
|
|
var len = input.length;
|
|
for(var i = 0; i < len; i += 3)
|
|
{
|
|
var triplet = (input.charCodeAt(i) << 16)
|
|
| (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
|
|
| (i + 2 < len ? input.charCodeAt(i+2) : 0);
|
|
for(var j = 0; j < 4; j++)
|
|
{
|
|
if(i * 8 + j * 6 > input.length * 8) output += b64pad;
|
|
else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Convert a raw string to an arbitrary string encoding
|
|
*/
|
|
function rstr2any(input, encoding)
|
|
{
|
|
var divisor = encoding.length;
|
|
var i, j, q, x, quotient;
|
|
|
|
/* Convert to an array of 16-bit big-endian values, forming the dividend */
|
|
var dividend = Array(Math.ceil(input.length / 2));
|
|
for(i = 0; i < dividend.length; i++)
|
|
{
|
|
dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
|
|
}
|
|
|
|
/*
|
|
* Repeatedly perform a long division. The binary array forms the dividend,
|
|
* the length of the encoding is the divisor. Once computed, the quotient
|
|
* forms the dividend for the next step. All remainders are stored for later
|
|
* use.
|
|
*/
|
|
var full_length = Math.ceil(input.length * 8 /
|
|
(Math.log(encoding.length) / Math.log(2)));
|
|
var remainders = Array(full_length);
|
|
for(j = 0; j < full_length; j++)
|
|
{
|
|
quotient = Array();
|
|
x = 0;
|
|
for(i = 0; i < dividend.length; i++)
|
|
{
|
|
x = (x << 16) + dividend[i];
|
|
q = Math.floor(x / divisor);
|
|
x -= q * divisor;
|
|
if(quotient.length > 0 || q > 0)
|
|
quotient[quotient.length] = q;
|
|
}
|
|
remainders[j] = x;
|
|
dividend = quotient;
|
|
}
|
|
|
|
/* Convert the remainders to the output string */
|
|
var output = "";
|
|
for(i = remainders.length - 1; i >= 0; i--)
|
|
output += encoding.charAt(remainders[i]);
|
|
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Encode a string as utf-8.
|
|
* For efficiency, this assumes the input is valid utf-16.
|
|
*/
|
|
function str2rstr_utf8(input)
|
|
{
|
|
var output = "";
|
|
var i = -1;
|
|
var x, y;
|
|
|
|
while(++i < input.length)
|
|
{
|
|
/* Decode utf-16 surrogate pairs */
|
|
x = input.charCodeAt(i);
|
|
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
|
|
if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
|
|
{
|
|
x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
|
|
i++;
|
|
}
|
|
|
|
/* Encode output as utf-8 */
|
|
if(x <= 0x7F)
|
|
output += String.fromCharCode(x);
|
|
else if(x <= 0x7FF)
|
|
output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
|
|
0x80 | ( x & 0x3F));
|
|
else if(x <= 0xFFFF)
|
|
output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
|
|
0x80 | ((x >>> 6 ) & 0x3F),
|
|
0x80 | ( x & 0x3F));
|
|
else if(x <= 0x1FFFFF)
|
|
output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
|
|
0x80 | ((x >>> 12) & 0x3F),
|
|
0x80 | ((x >>> 6 ) & 0x3F),
|
|
0x80 | ( x & 0x3F));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Encode a string as utf-16
|
|
*/
|
|
function str2rstr_utf16le(input)
|
|
{
|
|
var output = "";
|
|
for(var i = 0; i < input.length; i++)
|
|
output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
|
|
(input.charCodeAt(i) >>> 8) & 0xFF);
|
|
return output;
|
|
}
|
|
|
|
function str2rstr_utf16be(input)
|
|
{
|
|
var output = "";
|
|
for(var i = 0; i < input.length; i++)
|
|
output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
|
|
input.charCodeAt(i) & 0xFF);
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Convert a raw string to an array of little-endian words
|
|
* Characters >255 have their high-byte silently ignored.
|
|
*/
|
|
function rstr2binl(input)
|
|
{
|
|
var output = Array(input.length >> 2);
|
|
for(var i = 0; i < output.length; i++)
|
|
output[i] = 0;
|
|
for(var i = 0; i < input.length * 8; i += 8)
|
|
output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Convert an array of little-endian words to a string
|
|
*/
|
|
function binl2rstr(input)
|
|
{
|
|
var output = "";
|
|
for(var i = 0; i < input.length * 32; i += 8)
|
|
output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* Calculate the MD5 of an array of little-endian words, and a bit length.
|
|
*/
|
|
function binl_md5(x, len)
|
|
{
|
|
/* append padding */
|
|
x[len >> 5] |= 0x80 << ((len) % 32);
|
|
x[(((len + 64) >>> 9) << 4) + 14] = len;
|
|
|
|
var a = 1732584193;
|
|
var b = -271733879;
|
|
var c = -1732584194;
|
|
var d = 271733878;
|
|
|
|
for(var i = 0; i < x.length; i += 16)
|
|
{
|
|
var olda = a;
|
|
var oldb = b;
|
|
var oldc = c;
|
|
var oldd = d;
|
|
|
|
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
|
|
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
|
|
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
|
|
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
|
|
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
|
|
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
|
|
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
|
|
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
|
|
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
|
|
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
|
|
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
|
|
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
|
|
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
|
|
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
|
|
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
|
|
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
|
|
|
|
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
|
|
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
|
|
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
|
|
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
|
|
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
|
|
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
|
|
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
|
|
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
|
|
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
|
|
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
|
|
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
|
|
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
|
|
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
|
|
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
|
|
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
|
|
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
|
|
|
|
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
|
|
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
|
|
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
|
|
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
|
|
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
|
|
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
|
|
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
|
|
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
|
|
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
|
|
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
|
|
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
|
|
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
|
|
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
|
|
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
|
|
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
|
|
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
|
|
|
|
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
|
|
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
|
|
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
|
|
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
|
|
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
|
|
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
|
|
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
|
|
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
|
|
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
|
|
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
|
|
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
|
|
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
|
|
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
|
|
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
|
|
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
|
|
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
|
|
|
|
a = safe_add(a, olda);
|
|
b = safe_add(b, oldb);
|
|
c = safe_add(c, oldc);
|
|
d = safe_add(d, oldd);
|
|
}
|
|
return Array(a, b, c, d);
|
|
}
|
|
|
|
/*
|
|
* These functions implement the four basic operations the algorithm uses.
|
|
*/
|
|
function md5_cmn(q, a, b, x, s, t)
|
|
{
|
|
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
|
|
}
|
|
function md5_ff(a, b, c, d, x, s, t)
|
|
{
|
|
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
|
|
}
|
|
function md5_gg(a, b, c, d, x, s, t)
|
|
{
|
|
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
|
|
}
|
|
function md5_hh(a, b, c, d, x, s, t)
|
|
{
|
|
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
|
|
}
|
|
function md5_ii(a, b, c, d, x, s, t)
|
|
{
|
|
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
|
|
}
|
|
|
|
/*
|
|
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
|
* to work around bugs in some JS interpreters.
|
|
*/
|
|
function safe_add(x, y)
|
|
{
|
|
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
|
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
|
return (msw << 16) | (lsw & 0xFFFF);
|
|
}
|
|
|
|
/*
|
|
* Bitwise rotate a 32-bit number to the left.
|
|
*/
|
|
function bit_rol(num, cnt)
|
|
{
|
|
return (num << cnt) | (num >>> (32 - cnt));
|
|
}
|
|
|
|
|
|
/* #############################################################################
|
|
UTF-8 Decoder and Encoder
|
|
base64 Encoder and Decoder
|
|
written by Tobias Kieslich, justdreams
|
|
Contact: tobias@justdreams.de http://www.justdreams.de/
|
|
############################################################################# */
|
|
|
|
// returns an array of byterepresenting dezimal numbers which represent the
|
|
// plaintext in an UTF-8 encoded version. Expects a string.
|
|
// This function includes an exception management for those nasty browsers like
|
|
// NN401, which returns negative decimal numbers for chars>128. I hate it!!
|
|
// This handling is unfortunately limited to the user's charset. Anyway, it works
|
|
// in most of the cases! Special signs with an unicode>256 return numbers, which
|
|
// can not be converted to the actual unicode and so not to the valid utf-8
|
|
// representation. Anyway, this function does always return values which can not
|
|
// misinterpretd by RC4 or base64 en- or decoding, because every value is >0 and
|
|
// <255!!
|
|
// Arrays are faster and easier to handle in b64 encoding or encrypting....
|
|
function utf8t2d(t)
|
|
{
|
|
t = t.replace(/\r\n/g,"\n");
|
|
var d=new Array; var test=String.fromCharCode(237);
|
|
if (test.charCodeAt(0) < 0)
|
|
for(var n=0; n<t.length; n++)
|
|
{
|
|
var c=t.charCodeAt(n);
|
|
if (c>0)
|
|
d[d.length]= c;
|
|
else {
|
|
d[d.length]= (((256+c)>>6)|192);
|
|
d[d.length]= (((256+c)&63)|128);}
|
|
}
|
|
else
|
|
for(var n=0; n<t.length; n++)
|
|
{
|
|
var c=t.charCodeAt(n);
|
|
// all the signs of asci => 1byte
|
|
if (c<128)
|
|
d[d.length]= c;
|
|
// all the signs between 127 and 2047 => 2byte
|
|
else if((c>127) && (c<2048)) {
|
|
d[d.length]= ((c>>6)|192);
|
|
d[d.length]= ((c&63)|128);}
|
|
// all the signs between 2048 and 66536 => 3byte
|
|
else {
|
|
d[d.length]= ((c>>12)|224);
|
|
d[d.length]= (((c>>6)&63)|128);
|
|
d[d.length]= ((c&63)|128);}
|
|
}
|
|
return d;
|
|
}
|
|
|
|
// returns plaintext from an array of bytesrepresenting dezimal numbers, which
|
|
// represent an UTF-8 encoded text; browser which does not understand unicode
|
|
// like NN401 will show "?"-signs instead
|
|
// expects an array of byterepresenting decimals; returns a string
|
|
function utf8d2t(d)
|
|
{
|
|
var r=new Array; var i=0;
|
|
while(i<d.length)
|
|
{
|
|
if (d[i]<128) {
|
|
r[r.length]= String.fromCharCode(d[i]); i++;}
|
|
else if((d[i]>191) && (d[i]<224)) {
|
|
r[r.length]= String.fromCharCode(((d[i]&31)<<6) | (d[i+1]&63)); i+=2;}
|
|
else {
|
|
r[r.length]= String.fromCharCode(((d[i]&15)<<12) | ((d[i+1]&63)<<6) | (d[i+2]&63)); i+=3;}
|
|
}
|
|
return r.join("");
|
|
}
|
|
|
|
// included in <body onload="b64arrays"> it creates two arrays which makes base64
|
|
// en- and decoding faster
|
|
// this speed is noticeable especially when coding larger texts (>5k or so)
|
|
function b64arrays() {
|
|
var b64s='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
b64 = new Array();f64 =new Array();
|
|
for (var i=0; i<b64s.length ;i++) {
|
|
b64[i] = b64s.charAt(i);
|
|
f64[b64s.charAt(i)] = i;
|
|
}
|
|
}
|
|
|
|
// creates a base64 encoded text out of an array of byerepresenting dezimals
|
|
// it is really base64 :) this makes serversided handling easier
|
|
// expects an array; returns a string
|
|
function b64d2t(d) {
|
|
var r=new Array; var i=0; var dl=d.length;
|
|
// this is for the padding
|
|
if ((dl%3) == 1) {
|
|
d[d.length] = 0; d[d.length] = 0;}
|
|
if ((dl%3) == 2)
|
|
d[d.length] = 0;
|
|
// from here conversion
|
|
while (i<d.length)
|
|
{
|
|
r[r.length] = b64[d[i]>>2];
|
|
r[r.length] = b64[((d[i]&3)<<4) | (d[i+1]>>4)];
|
|
r[r.length] = b64[((d[i+1]&15)<<2) | (d[i+2]>>6)];
|
|
r[r.length] = b64[d[i+2]&63];
|
|
i+=3;
|
|
}
|
|
// this is again for the padding
|
|
if ((dl%3) == 1)
|
|
r[r.length-1] = r[r.length-2] = "=";
|
|
if ((dl%3) == 2)
|
|
r[r.length-1] = "=";
|
|
// we join the array to return a textstring
|
|
var t=r.join("");
|
|
return t;
|
|
}
|
|
|
|
// returns array of byterepresenting numbers created of an base64 encoded text
|
|
// it is still the slowest function in this modul; I hope I can make it faster
|
|
// expects string; returns an array
|
|
function b64t2d(t) {
|
|
var d=new Array; var i=0;
|
|
// here we fix this CRLF sequenz created by MS-OS; arrrgh!!!
|
|
t=t.replace(/\n|\r/g,""); t=t.replace(/=/g,"");
|
|
while (i<t.length)
|
|
{
|
|
d[d.length] = (f64[t.charAt(i)]<<2) | (f64[t.charAt(i+1)]>>4);
|
|
d[d.length] = (((f64[t.charAt(i+1)]&15)<<4) | (f64[t.charAt(i+2)]>>2));
|
|
d[d.length] = (((f64[t.charAt(i+2)]&3)<<6) | (f64[t.charAt(i+3)]));
|
|
i+=4;
|
|
}
|
|
if (t.length%4 == 2)
|
|
d = d.slice(0, d.length-2);
|
|
if (t.length%4 == 3)
|
|
d = d.slice(0, d.length-1);
|
|
return d;
|
|
}
|
|
|
|
if (typeof(atob) == 'undefined' || typeof(btoa) == 'undefined')
|
|
b64arrays();
|
|
|
|
if (typeof(atob) == 'undefined') {
|
|
b64decode = function(s) {
|
|
return utf8d2t(b64t2d(s));
|
|
}
|
|
} else {
|
|
b64decode = function(s) {
|
|
return decodeURIComponent(escape(atob(s)));
|
|
}
|
|
}
|
|
|
|
if (typeof(btoa) == 'undefined') {
|
|
b64encode = function(s) {
|
|
return b64d2t(utf8t2d(s));
|
|
}
|
|
} else {
|
|
b64encode = function(s) {
|
|
return btoa(unescape(encodeURIComponent(s)));
|
|
}
|
|
}
|
|
|
|
function cnonce(size) {
|
|
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
var cnonce = '';
|
|
for (var i=0; i<size; i++) {
|
|
cnonce += tab.charAt(Math.round(Math.random(new Date().getTime())*(tab.length-1)));
|
|
}
|
|
return cnonce;
|
|
}
|
|
|
|
|
|
JSJAC_HAVEKEYS = true; // whether to use keys
|
|
JSJAC_NKEYS = 16; // number of keys to generate
|
|
JSJAC_INACTIVITY = 300; // qnd hack to make suspend/resume
|
|
// work more smoothly with polling
|
|
JSJAC_ERR_COUNT = 10; // number of retries in case of connection
|
|
// errors
|
|
|
|
JSJAC_ALLOW_PLAIN = true; // whether to allow plaintext logins
|
|
|
|
JSJAC_CHECKQUEUEINTERVAL = 100; // msecs to poll send queue
|
|
JSJAC_CHECKINQUEUEINTERVAL = 100; // msecs to poll incoming queue
|
|
JSJAC_TIMERVAL = 2000; // default polling interval
|
|
|
|
// Options specific to HTTP Binding (BOSH)
|
|
JSJACHBC_MAX_HOLD = 1; // default for number of connections held by
|
|
// connection manager
|
|
JSJACHBC_MAX_WAIT = 20; // default 'wait' param - how long an idle connection
|
|
// should be held by connection manager
|
|
|
|
JSJACHBC_BOSH_VERSION = "1.6";
|
|
JSJACHBC_USE_BOSH_VER = true;
|
|
|
|
JSJACHBC_MAXPAUSE = 20; // how long a suspend/resume cycle may take
|
|
|
|
/*** END CONFIG ***/
|
|
|
|
|
|
/* Copyright (c) 2005-2007 Sam Stephenson
|
|
*
|
|
* 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 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.
|
|
*/
|
|
|
|
/*
|
|
json.js
|
|
taken from prototype.js, made static
|
|
*/
|
|
function JSJaCJSON() {}
|
|
JSJaCJSON.toString = function (obj) {
|
|
var m = {
|
|
'\b': '\\b',
|
|
'\t': '\\t',
|
|
'\n': '\\n',
|
|
'\f': '\\f',
|
|
'\r': '\\r',
|
|
'"' : '\\"',
|
|
'\\': '\\\\'
|
|
},
|
|
s = {
|
|
array: function (x) {
|
|
var a = ['['], b, f, i, l = x.length, v;
|
|
for (i = 0; i < l; i += 1) {
|
|
v = x[i];
|
|
f = s[typeof v];
|
|
if (f) {
|
|
try {
|
|
v = f(v);
|
|
if (typeof v == 'string') {
|
|
if (b) {
|
|
a[a.length] = ',';
|
|
}
|
|
a[a.length] = v;
|
|
b = true;
|
|
}
|
|
} catch(e) {
|
|
}
|
|
}
|
|
}
|
|
a[a.length] = ']';
|
|
return a.join('');
|
|
},
|
|
'boolean': function (x) {
|
|
return String(x);
|
|
},
|
|
'null': function (x) {
|
|
return "null";
|
|
},
|
|
number: function (x) {
|
|
return isFinite(x) ? String(x) : 'null';
|
|
},
|
|
object: function (x) {
|
|
if (x) {
|
|
if (x instanceof Array) {
|
|
return s.array(x);
|
|
}
|
|
var a = ['{'], b, f, i, v;
|
|
for (i in x) {
|
|
if (x.hasOwnProperty(i)) {
|
|
v = x[i];
|
|
f = s[typeof v];
|
|
if (f) {
|
|
try {
|
|
v = f(v);
|
|
if (typeof v == 'string') {
|
|
if (b) {
|
|
a[a.length] = ',';
|
|
}
|
|
a.push(s.string(i), ':', v);
|
|
b = true;
|
|
}
|
|
} catch(e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
a[a.length] = '}';
|
|
return a.join('');
|
|
}
|
|
return 'null';
|
|
},
|
|
string: function (x) {
|
|
if (/["\\\x00-\x1f]/.test(x)) {
|
|
x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
|
|
var c = m[b];
|
|
if (c) {
|
|
return c;
|
|
}
|
|
c = b.charCodeAt();
|
|
return '\\u00' +
|
|
Math.floor(c / 16).toString(16) +
|
|
(c % 16).toString(16);
|
|
});
|
|
}
|
|
return '"' + x + '"';
|
|
}
|
|
};
|
|
|
|
switch (typeof(obj)) {
|
|
case 'object':
|
|
return s.object(obj);
|
|
case 'array':
|
|
return s.array(obj);
|
|
|
|
}
|
|
};
|
|
|
|
JSJaCJSON.parse = function (str) {
|
|
try {
|
|
return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
|
|
str.replace(/"(\\.|[^"\\])*"/g, ''))) &&
|
|
eval('(' + str + ')');
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @fileoverview This file contains all things that make life easier when
|
|
* dealing with JIDs
|
|
* @author Stefan Strigler
|
|
* @version $Revision$
|
|
*/
|
|
|
|
/**
|
|
* list of forbidden chars for nodenames
|
|
* @private
|
|
*/
|
|
var JSJACJID_FORBIDDEN = ['"',' ','&','\'','/',':','<','>','@'];
|
|
|
|
/**
|
|
* Creates a new JSJaCJID object
|
|
* @class JSJaCJID models xmpp jid objects
|
|
* @constructor
|
|
* @param {Object} jid jid may be either of type String or a JID represented
|
|
* by JSON with fields 'node', 'domain' and 'resource'
|
|
* @throws JSJaCJIDInvalidException Thrown if jid is not valid
|
|
* @return a new JSJaCJID object
|
|
*/
|
|
function JSJaCJID(jid) {
|
|
/**
|
|
*@private
|
|
*/
|
|
this._node = '';
|
|
/**
|
|
*@private
|
|
*/
|
|
this._domain = '';
|
|
/**
|
|
*@private
|
|
*/
|
|
this._resource = '';
|
|
|
|
if (typeof(jid) == 'string') {
|
|
if (jid.indexOf('@') != -1) {
|
|
this.setNode(jid.substring(0,jid.indexOf('@')));
|
|
jid = jid.substring(jid.indexOf('@')+1);
|
|
}
|
|
if (jid.indexOf('/') != -1) {
|
|
this.setResource(jid.substring(jid.indexOf('/')+1));
|
|
jid = jid.substring(0,jid.indexOf('/'));
|
|
}
|
|
this.setDomain(jid);
|
|
} else {
|
|
this.setNode(jid.node);
|
|
this.setDomain(jid.domain);
|
|
this.setResource(jid.resource);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the node part of the jid
|
|
* @return A string representing the node name
|
|
* @type String
|
|
*/
|
|
JSJaCJID.prototype.getNode = function() { return this._node; };
|
|
|
|
/**
|
|
* Gets the domain part of the jid
|
|
* @return A string representing the domain name
|
|
* @type String
|
|
*/
|
|
JSJaCJID.prototype.getDomain = function() { return this._domain; };
|
|
|
|
/**
|
|
* Gets the resource part of the jid
|
|
* @return A string representing the resource
|
|
* @type String
|
|
*/
|
|
JSJaCJID.prototype.getResource = function() { return this._resource; };
|
|
|
|
|
|
/**
|
|
* Sets the node part of the jid
|
|
* @param {String} node Name of the node
|
|
* @throws JSJaCJIDInvalidException Thrown if node name contains invalid chars
|
|
* @return This object
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCJID.prototype.setNode = function(node) {
|
|
JSJaCJID._checkNodeName(node);
|
|
this._node = node || '';
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets the domain part of the jid
|
|
* @param {String} domain Name of the domain
|
|
* @throws JSJaCJIDInvalidException Thrown if domain name contains invalid
|
|
* chars or is empty
|
|
* @return This object
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCJID.prototype.setDomain = function(domain) {
|
|
if (!domain || domain == '')
|
|
throw new JSJaCJIDInvalidException("domain name missing");
|
|
// chars forbidden for a node are not allowed in domain names
|
|
// anyway, so let's check
|
|
JSJaCJID._checkNodeName(domain);
|
|
this._domain = domain;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets the resource part of the jid
|
|
* @param {String} resource Name of the resource
|
|
* @return This object
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCJID.prototype.setResource = function(resource) {
|
|
this._resource = resource || '';
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* The string representation of the full jid
|
|
* @return A string representing the jid
|
|
* @type String
|
|
*/
|
|
JSJaCJID.prototype.toString = function() {
|
|
var jid = '';
|
|
if (this.getNode() && this.getNode() != '')
|
|
jid = this.getNode() + '@';
|
|
jid += this.getDomain(); // we always have a domain
|
|
if (this.getResource() && this.getResource() != "")
|
|
jid += '/' + this.getResource();
|
|
return jid;
|
|
};
|
|
|
|
/**
|
|
* Removes the resource part of the jid
|
|
* @return This object
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCJID.prototype.removeResource = function() {
|
|
return this.setResource();
|
|
};
|
|
|
|
/**
|
|
* creates a copy of this JSJaCJID object
|
|
* @return A copy of this
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCJID.prototype.clone = function() {
|
|
return new JSJaCJID(this.toString());
|
|
};
|
|
|
|
/**
|
|
* Compares two jids if they belong to the same entity (i.e. w/o resource)
|
|
* @param {String} jid a jid as string or JSJaCJID object
|
|
* @return 'true' if jid is same entity as this
|
|
* @type Boolean
|
|
*/
|
|
JSJaCJID.prototype.isEntity = function(jid) {
|
|
if (typeof jid == 'string')
|
|
jid = (new JSJaCJID(jid));
|
|
jid.removeResource();
|
|
return (this.clone().removeResource().toString() === jid.toString());
|
|
};
|
|
|
|
/**
|
|
* Check if node name is valid
|
|
* @private
|
|
* @param {String} node A name for a node
|
|
* @throws JSJaCJIDInvalidException Thrown if name for node is not allowed
|
|
*/
|
|
JSJaCJID._checkNodeName = function(nodeprep) {
|
|
if (!nodeprep || nodeprep == '')
|
|
return;
|
|
for (var i=0; i< JSJACJID_FORBIDDEN.length; i++) {
|
|
if (nodeprep.indexOf(JSJACJID_FORBIDDEN[i]) != -1) {
|
|
throw new JSJaCJIDInvalidException("forbidden char in nodename: "+JSJACJID_FORBIDDEN[i]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a new Exception of type JSJaCJIDInvalidException
|
|
* @class Exception to indicate invalid values for a jid
|
|
* @constructor
|
|
* @param {String} message The message associated with this Exception
|
|
*/
|
|
function JSJaCJIDInvalidException(message) {
|
|
/**
|
|
* The exceptions associated message
|
|
* @type String
|
|
*/
|
|
this.message = message;
|
|
/**
|
|
* The name of the exception
|
|
* @type String
|
|
*/
|
|
this.name = "JSJaCJIDInvalidException";
|
|
}
|
|
|
|
|
|
/* Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/**
|
|
* @private
|
|
* This code is taken from {@link
|
|
* http://wiki.script.aculo.us/scriptaculous/show/Builder
|
|
* script.aculo.us' Dom Builder} and has been modified to suit our
|
|
* needs.<br/>
|
|
* The original parts of the code do have the following
|
|
* copyright and license notice:<br/>
|
|
* Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us,
|
|
* http://mir.acu lo.us) <br/>
|
|
* script.aculo.us is freely distributable under the terms of an
|
|
* MIT-style license.<br>
|
|
* For details, see the script.aculo.us web site:
|
|
* http://script.aculo.us/<br>
|
|
*/
|
|
var JSJaCBuilder = {
|
|
/**
|
|
* @private
|
|
*/
|
|
buildNode: function(doc, elementName) {
|
|
|
|
var element, ns = arguments[4];
|
|
|
|
// attributes (or text)
|
|
if(arguments[2])
|
|
if(JSJaCBuilder._isStringOrNumber(arguments[2]) ||
|
|
(arguments[2] instanceof Array)) {
|
|
element = this._createElement(doc, elementName, ns);
|
|
JSJaCBuilder._children(doc, element, arguments[2]);
|
|
} else {
|
|
ns = arguments[2]['xmlns'] || ns;
|
|
element = this._createElement(doc, elementName, ns);
|
|
for(attr in arguments[2]) {
|
|
if (arguments[2].hasOwnProperty(attr) && attr != 'xmlns')
|
|
element.setAttribute(attr, arguments[2][attr]);
|
|
}
|
|
}
|
|
else
|
|
element = this._createElement(doc, elementName, ns);
|
|
// text, or array of children
|
|
if(arguments[3])
|
|
JSJaCBuilder._children(doc, element, arguments[3], ns);
|
|
|
|
return element;
|
|
},
|
|
|
|
_createElement: function(doc, elementName, ns) {
|
|
try {
|
|
if (ns)
|
|
return doc.createElementNS(ns, elementName);
|
|
} catch (ex) { }
|
|
|
|
var el = doc.createElement(elementName);
|
|
|
|
if (ns)
|
|
el.setAttribute("xmlns", ns);
|
|
|
|
return el;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_text: function(doc, text) {
|
|
return doc.createTextNode(text);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_children: function(doc, element, children, ns) {
|
|
if(typeof children=='object') { // array can hold nodes and text
|
|
for (var i in children) {
|
|
if (children.hasOwnProperty(i)) {
|
|
var e = children[i];
|
|
if (typeof e=='object') {
|
|
if (e instanceof Array) {
|
|
var node = JSJaCBuilder.buildNode(doc, e[0], e[1], e[2], ns);
|
|
element.appendChild(node);
|
|
} else {
|
|
element.appendChild(e);
|
|
}
|
|
} else {
|
|
if(JSJaCBuilder._isStringOrNumber(e)) {
|
|
element.appendChild(JSJaCBuilder._text(doc, e));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if(JSJaCBuilder._isStringOrNumber(children)) {
|
|
element.appendChild(JSJaCBuilder._text(doc, children));
|
|
}
|
|
}
|
|
},
|
|
|
|
_attributes: function(attributes) {
|
|
var attrs = [];
|
|
for(attribute in attributes)
|
|
if (attributes.hasOwnProperty(attribute))
|
|
attrs.push(attribute +
|
|
'="' + attributes[attribute].toString().htmlEnc() + '"');
|
|
return attrs.join(" ");
|
|
},
|
|
|
|
_isStringOrNumber: function(param) {
|
|
return(typeof param=='string' || typeof param=='number');
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @fileoverview Contains all Jabber/XMPP packet related classes.
|
|
* @author Stefan Strigler steve@zeank.in-berlin.de
|
|
* @version $Revision$
|
|
*/
|
|
|
|
var JSJACPACKET_USE_XMLNS = true;
|
|
|
|
/**
|
|
* Creates a new packet with given root tag name (for internal use)
|
|
* @class Somewhat abstract base class for all kinds of specialised packets
|
|
* @param {String} name The root tag name of the packet
|
|
* (i.e. one of 'message', 'iq' or 'presence')
|
|
*/
|
|
function JSJaCPacket(name) {
|
|
/**
|
|
* @private
|
|
*/
|
|
this.name = name;
|
|
|
|
if (typeof(JSJACPACKET_USE_XMLNS) != 'undefined' && JSJACPACKET_USE_XMLNS)
|
|
/**
|
|
* @private
|
|
*/
|
|
this.doc = XmlDocument.create(name,'jabber:client');
|
|
else
|
|
/**
|
|
* @private
|
|
*/
|
|
this.doc = XmlDocument.create(name,'');
|
|
}
|
|
|
|
/**
|
|
* Gets the type (name of root element) of this packet, i.e. one of
|
|
* 'presence', 'message' or 'iq'
|
|
* @return the top level tag name
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.pType = function() { return this.name; };
|
|
|
|
/**
|
|
* Gets the associated Document for this packet.
|
|
* @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#i-Document Document}
|
|
*/
|
|
JSJaCPacket.prototype.getDoc = function() {
|
|
return this.doc;
|
|
};
|
|
/**
|
|
* Gets the root node of this packet
|
|
* @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
|
|
*/
|
|
JSJaCPacket.prototype.getNode = function() {
|
|
if (this.getDoc() && this.getDoc().documentElement)
|
|
return this.getDoc().documentElement;
|
|
else
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Sets the 'to' attribute of the root node of this packet
|
|
* @param {String} to
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.setTo = function(to) {
|
|
if (!to || to == '')
|
|
this.getNode().removeAttribute('to');
|
|
else if (typeof(to) == 'string')
|
|
this.getNode().setAttribute('to',to);
|
|
else
|
|
this.getNode().setAttribute('to',to.toString());
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the 'from' attribute of the root node of this
|
|
* packet. Usually this is not needed as the server will take care
|
|
* of this automatically.
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.setFrom = function(from) {
|
|
if (!from || from == '')
|
|
this.getNode().removeAttribute('from');
|
|
else if (typeof(from) == 'string')
|
|
this.getNode().setAttribute('from',from);
|
|
else
|
|
this.getNode().setAttribute('from',from.toString());
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets 'id' attribute of the root node of this packet.
|
|
* @param {String} id The id of the packet.
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.setID = function(id) {
|
|
if (!id || id == '')
|
|
this.getNode().removeAttribute('id');
|
|
else
|
|
this.getNode().setAttribute('id',id);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the 'type' attribute of the root node of this packet.
|
|
* @param {String} type The type of the packet.
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.setType = function(type) {
|
|
if (!type || type == '')
|
|
this.getNode().removeAttribute('type');
|
|
else
|
|
this.getNode().setAttribute('type',type);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets 'xml:lang' for this packet
|
|
* @param {String} xmllang The xml:lang of the packet.
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.setXMLLang = function(xmllang) {
|
|
// Fix IE bug with xml:lang attribute
|
|
|
|
// Also due to issues with both BD and jQuery being used, employ a simple regexp since the detection
|
|
// here is very limited.
|
|
if (navigator.appVersion.match(/^.*MSIE (\d)/))
|
|
return this;
|
|
if (!xmllang || xmllang == '')
|
|
this.getNode().removeAttribute('xml:lang');
|
|
else
|
|
this.getNode().setAttribute('xml:lang',xmllang);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Gets the 'to' attribute of this packet
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getTo = function() {
|
|
return this.getNode().getAttribute('to');
|
|
};
|
|
/**
|
|
* Gets the 'from' attribute of this packet.
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getFrom = function() {
|
|
return this.getNode().getAttribute('from');
|
|
};
|
|
/**
|
|
* Gets the 'to' attribute of this packet as a JSJaCJID object
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCPacket.prototype.getToJID = function() {
|
|
return new JSJaCJID(this.getTo());
|
|
};
|
|
/**
|
|
* Gets the 'from' attribute of this packet as a JSJaCJID object
|
|
* @type JSJaCJID
|
|
*/
|
|
JSJaCPacket.prototype.getFromJID = function() {
|
|
return new JSJaCJID(this.getFrom());
|
|
};
|
|
/**
|
|
* Gets the 'id' of this packet
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getID = function() {
|
|
return this.getNode().getAttribute('id');
|
|
};
|
|
/**
|
|
* Gets the 'type' of this packet
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getType = function() {
|
|
return this.getNode().getAttribute('type');
|
|
};
|
|
/**
|
|
* Gets the 'xml:lang' of this packet
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getXMLLang = function() {
|
|
return this.getNode().getAttribute('xml:lang');
|
|
};
|
|
/**
|
|
* Gets the 'xmlns' (xml namespace) of the root node of this packet
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getXMLNS = function() {
|
|
return this.getNode().namespaceURI || this.getNode().getAttribute('xmlns');
|
|
};
|
|
|
|
/**
|
|
* Gets a child element of this packet. If no params given returns first child.
|
|
* @param {String} name Tagname of child to retrieve. Use '*' to match any tag. [optional]
|
|
* @param {String} ns Namespace of child. Use '*' to match any ns.[optional]
|
|
* @return The child node, null if none found
|
|
* @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
|
|
*/
|
|
JSJaCPacket.prototype.getChild = function(name, ns) {
|
|
if (!this.getNode()) {
|
|
return null;
|
|
}
|
|
|
|
name = name || '*';
|
|
ns = ns || '*';
|
|
|
|
if (this.getNode().getElementsByTagNameNS) {
|
|
return this.getNode().getElementsByTagNameNS(ns, name).item(0);
|
|
}
|
|
|
|
// fallback
|
|
var nodes = this.getNode().getElementsByTagName(name);
|
|
if (ns != '*') {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
if (nodes.item(i).namespaceURI == ns || nodes.item(i).getAttribute('xmlns') == ns) {
|
|
return nodes.item(i);
|
|
}
|
|
}
|
|
} else {
|
|
return nodes.item(0);
|
|
}
|
|
return null; // nothing found
|
|
};
|
|
|
|
/**
|
|
* Gets the node value of a child element of this packet.
|
|
* @param {String} name Tagname of child to retrieve.
|
|
* @param {String} ns Namespace of child
|
|
* @return The value of the child node, empty string if none found
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.getChildVal = function(name, ns) {
|
|
var node = this.getChild(name, ns);
|
|
var ret = '';
|
|
if (node && node.hasChildNodes()) {
|
|
// concatenate all values from childNodes
|
|
for (var i=0; i<node.childNodes.length; i++)
|
|
if (node.childNodes.item(i).nodeValue)
|
|
ret += node.childNodes.item(i).nodeValue;
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* Returns a copy of this node
|
|
* @return a copy of this node
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.clone = function() {
|
|
return JSJaCPacket.wrapNode(this.getNode());
|
|
};
|
|
|
|
/**
|
|
* Checks if packet is of type 'error'
|
|
* @return 'true' if this packet is of type 'error', 'false' otherwise
|
|
* @type boolean
|
|
*/
|
|
JSJaCPacket.prototype.isError = function() {
|
|
return (this.getType() == 'error');
|
|
};
|
|
|
|
/**
|
|
* Returns an error condition reply according to {@link http://www.xmpp.org/extensions/xep-0086.html XEP-0086}. Creates a clone of the calling packet with senders and recipient exchanged and error stanza appended.
|
|
* @param {STANZA_ERROR} stanza_error an error stanza containing error cody, type and condition of the error to be indicated
|
|
* @return an error reply packet
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.errorReply = function(stanza_error) {
|
|
var rPacket = this.clone();
|
|
rPacket.setTo(this.getFrom());
|
|
rPacket.setFrom();
|
|
rPacket.setType('error');
|
|
|
|
rPacket.appendNode('error',
|
|
{code: stanza_error.code, type: stanza_error.type},
|
|
[[stanza_error.cond]]);
|
|
|
|
return rPacket;
|
|
};
|
|
|
|
/**
|
|
* Returns a string representation of the raw xml content of this packet.
|
|
* @type String
|
|
*/
|
|
JSJaCPacket.prototype.xml = typeof XMLSerializer != 'undefined' ?
|
|
function() {
|
|
var r = (new XMLSerializer()).serializeToString(this.getNode());
|
|
if (typeof(r) == 'undefined')
|
|
r = (new XMLSerializer()).serializeToString(this.doc); // oldschool
|
|
return r
|
|
} :
|
|
function() {// IE
|
|
return this.getDoc().xml
|
|
};
|
|
|
|
|
|
// PRIVATE METHODS DOWN HERE
|
|
|
|
/**
|
|
* Gets an attribute of the root element
|
|
* @private
|
|
*/
|
|
JSJaCPacket.prototype._getAttribute = function(attr) {
|
|
return this.getNode().getAttribute(attr);
|
|
};
|
|
|
|
|
|
if (document.ELEMENT_NODE == null) {
|
|
document.ELEMENT_NODE = 1;
|
|
document.ATTRIBUTE_NODE = 2;
|
|
document.TEXT_NODE = 3;
|
|
document.CDATA_SECTION_NODE = 4;
|
|
document.ENTITY_REFERENCE_NODE = 5;
|
|
document.ENTITY_NODE = 6;
|
|
document.PROCESSING_INSTRUCTION_NODE = 7;
|
|
document.COMMENT_NODE = 8;
|
|
document.DOCUMENT_NODE = 9;
|
|
document.DOCUMENT_TYPE_NODE = 10;
|
|
document.DOCUMENT_FRAGMENT_NODE = 11;
|
|
document.NOTATION_NODE = 12;
|
|
}
|
|
|
|
/**
|
|
* import node into this packets document
|
|
* @private
|
|
*/
|
|
JSJaCPacket.prototype._importNode = function(node, allChildren) {
|
|
switch (node.nodeType) {
|
|
case document.ELEMENT_NODE:
|
|
|
|
if (this.getDoc().createElementNS) {
|
|
var newNode = this.getDoc().createElementNS(node.namespaceURI, node.nodeName);
|
|
} else {
|
|
var newNode = this.getDoc().createElement(node.nodeName);
|
|
}
|
|
|
|
/* does the node have any attributes to add? */
|
|
if (node.attributes && node.attributes.length > 0)
|
|
for (var i = 0, il = node.attributes.length;i < il; i++) {
|
|
var attr = node.attributes.item(i);
|
|
if (attr.nodeName == 'xmlns' && newNode.getAttribute('xmlns') != null ) continue;
|
|
if (newNode.setAttributeNS && attr.namespaceURI) {
|
|
newNode.setAttributeNS(attr.namespaceURI,
|
|
attr.nodeName,
|
|
attr.nodeValue);
|
|
} else {
|
|
newNode.setAttribute(attr.nodeName,
|
|
attr.nodeValue);
|
|
}
|
|
}
|
|
/* are we going after children too, and does the node have any? */
|
|
if (allChildren && node.childNodes && node.childNodes.length > 0) {
|
|
for (var i = 0, il = node.childNodes.length; i < il; i++) {
|
|
newNode.appendChild(this._importNode(node.childNodes.item(i), allChildren));
|
|
}
|
|
}
|
|
return newNode;
|
|
break;
|
|
case document.TEXT_NODE:
|
|
case document.CDATA_SECTION_NODE:
|
|
case document.COMMENT_NODE:
|
|
return this.getDoc().createTextNode(node.nodeValue);
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set node value of a child node
|
|
* @private
|
|
*/
|
|
JSJaCPacket.prototype._setChildNode = function(nodeName, nodeValue) {
|
|
var aNode = this.getChild(nodeName);
|
|
var tNode = this.getDoc().createTextNode(nodeValue);
|
|
if (aNode)
|
|
try {
|
|
aNode.replaceChild(tNode,aNode.firstChild);
|
|
} catch (e) { }
|
|
else {
|
|
try {
|
|
aNode = this.getDoc().createElementNS(this.getNode().namespaceURI,
|
|
nodeName);
|
|
} catch (ex) {
|
|
aNode = this.getDoc().createElement(nodeName)
|
|
}
|
|
this.getNode().appendChild(aNode);
|
|
aNode.appendChild(tNode);
|
|
}
|
|
return aNode;
|
|
};
|
|
|
|
/**
|
|
* Builds a node using {@link
|
|
* http://wiki.script.aculo.us/scriptaculous/show/Builder
|
|
* script.aculo.us' Dom Builder} notation.
|
|
* This code is taken from {@link
|
|
* http://wiki.script.aculo.us/scriptaculous/show/Builder
|
|
* script.aculo.us' Dom Builder} and has been modified to suit our
|
|
* needs.<br/>
|
|
* The original parts of the code do have the following copyright
|
|
* and license notice:<br/>
|
|
* Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us,
|
|
* http://mir.acu lo.us) <br/>
|
|
* script.aculo.us is freely distributable under the terms of an
|
|
* MIT-style licen se. // For details, see the script.aculo.us web
|
|
* site: http://script.aculo.us/<br>
|
|
* @author Thomas Fuchs
|
|
* @author Stefan Strigler
|
|
* @return The newly created node
|
|
* @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
|
|
*/
|
|
JSJaCPacket.prototype.buildNode = function(elementName) {
|
|
return JSJaCBuilder.buildNode(this.getDoc(),
|
|
elementName,
|
|
arguments[1],
|
|
arguments[2]);
|
|
};
|
|
|
|
/**
|
|
* Appends node created by buildNode to this packets parent node.
|
|
* @param {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node} element The node to append or
|
|
* @param {String} element A name plus an object hash with attributes (optional) plus an array of childnodes (optional)
|
|
* @see #buildNode
|
|
* @return This packet
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.prototype.appendNode = function(element) {
|
|
if (typeof element=='object') { // seems to be a prebuilt node
|
|
return this.getNode().appendChild(element)
|
|
} else { // build node
|
|
return this.getNode().appendChild(this.buildNode(element,
|
|
arguments[1],
|
|
arguments[2],
|
|
null,
|
|
this.getNode().namespaceURI));
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* A jabber/XMPP presence packet
|
|
* @class Models the XMPP notion of a 'presence' packet
|
|
* @extends JSJaCPacket
|
|
*/
|
|
function JSJaCPresence() {
|
|
/**
|
|
* @ignore
|
|
*/
|
|
this.base = JSJaCPacket;
|
|
this.base('presence');
|
|
}
|
|
JSJaCPresence.prototype = new JSJaCPacket;
|
|
|
|
/**
|
|
* Sets the status message for current status. Usually this is set
|
|
* to some human readable string indicating what the user is
|
|
* doing/feel like currently.
|
|
* @param {String} status A status message
|
|
* @return this
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPresence.prototype.setStatus = function(status) {
|
|
this._setChildNode("status", status);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the online status for this presence packet.
|
|
* @param {String} show An XMPP complient status indicator. Must
|
|
* be one of 'chat', 'away', 'xa', 'dnd'
|
|
* @return this
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPresence.prototype.setShow = function(show) {
|
|
if (show == 'chat' || show == 'away' || show == 'xa' || show == 'dnd')
|
|
this._setChildNode("show",show);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the priority of the resource bind to with this connection
|
|
* @param {int} prio The priority to set this resource to
|
|
* @return this
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPresence.prototype.setPriority = function(prio) {
|
|
this._setChildNode("priority", prio);
|
|
return this;
|
|
};
|
|
/**
|
|
* Some combined method that allowes for setting show, status and
|
|
* priority at once
|
|
* @param {String} show A status message
|
|
* @param {String} status A status indicator as defined by XMPP
|
|
* @param {int} prio A priority for this resource
|
|
* @return this
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPresence.prototype.setPresence = function(show,status,prio) {
|
|
if (show)
|
|
this.setShow(show);
|
|
if (status)
|
|
this.setStatus(status);
|
|
if (prio)
|
|
this.setPriority(prio);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Gets the status message of this presence
|
|
* @return The (human readable) status message
|
|
* @type String
|
|
*/
|
|
JSJaCPresence.prototype.getStatus = function() {
|
|
return this.getChildVal('status');
|
|
};
|
|
/**
|
|
* Gets the status of this presence.
|
|
* Either one of 'chat', 'away', 'xa' or 'dnd' or null.
|
|
* @return The status indicator as defined by XMPP
|
|
* @type String
|
|
*/
|
|
JSJaCPresence.prototype.getShow = function() {
|
|
return this.getChildVal('show');
|
|
};
|
|
/**
|
|
* Gets the priority of this status message
|
|
* @return A resource priority
|
|
* @type int
|
|
*/
|
|
JSJaCPresence.prototype.getPriority = function() {
|
|
return this.getChildVal('priority');
|
|
};
|
|
|
|
|
|
/**
|
|
* A jabber/XMPP iq packet
|
|
* @class Models the XMPP notion of an 'iq' packet
|
|
* @extends JSJaCPacket
|
|
*/
|
|
function JSJaCIQ() {
|
|
/**
|
|
* @ignore
|
|
*/
|
|
this.base = JSJaCPacket;
|
|
this.base('iq');
|
|
}
|
|
JSJaCIQ.prototype = new JSJaCPacket;
|
|
|
|
/**
|
|
* Some combined method to set 'to', 'type' and 'id' at once
|
|
* @param {String} to the recepients JID
|
|
* @param {String} type A XMPP compliant iq type (one of 'set', 'get', 'result' and 'error'
|
|
* @param {String} id A packet ID
|
|
* @return this
|
|
* @type JSJaCIQ
|
|
*/
|
|
JSJaCIQ.prototype.setIQ = function(to,type,id) {
|
|
if (to)
|
|
this.setTo(to);
|
|
if (type)
|
|
this.setType(type);
|
|
if (id)
|
|
this.setID(id);
|
|
return this;
|
|
};
|
|
/**
|
|
* Creates a 'query' child node with given XMLNS
|
|
* @param {String} xmlns The namespace for the 'query' node
|
|
* @return The query node
|
|
* @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
|
|
*/
|
|
JSJaCIQ.prototype.setQuery = function(xmlns) {
|
|
var query;
|
|
try {
|
|
query = this.getDoc().createElementNS(xmlns,'query');
|
|
} catch (e) {
|
|
query = this.getDoc().createElement('query');
|
|
query.setAttribute('xmlns',xmlns);
|
|
}
|
|
this.getNode().appendChild(query);
|
|
return query;
|
|
};
|
|
|
|
/**
|
|
* Gets the 'query' node of this packet
|
|
* @return The query node
|
|
* @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
|
|
*/
|
|
JSJaCIQ.prototype.getQuery = function() {
|
|
return this.getNode().getElementsByTagName('query').item(0);
|
|
};
|
|
/**
|
|
* Gets the XMLNS of the query node contained within this packet
|
|
* @return The namespace of the query node
|
|
* @type String
|
|
*/
|
|
JSJaCIQ.prototype.getQueryXMLNS = function() {
|
|
if (this.getQuery()) {
|
|
return this.getQuery().namespaceURI || this.getQuery().getAttribute('xmlns');
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates an IQ reply with type set to 'result'. If given appends payload to first child if IQ. Payload maybe XML as string or a DOM element (or an array of such elements as well).
|
|
* @param {Element} payload A payload to be appended [optional]
|
|
* @return An IQ reply packet
|
|
* @type JSJaCIQ
|
|
*/
|
|
JSJaCIQ.prototype.reply = function(payload) {
|
|
var rIQ = this.clone();
|
|
rIQ.setTo(this.getFrom());
|
|
rIQ.setFrom();
|
|
rIQ.setType('result');
|
|
if (payload) {
|
|
if (typeof payload == 'string')
|
|
rIQ.getChild().appendChild(rIQ.getDoc().loadXML(payload));
|
|
else if (payload.constructor == Array) {
|
|
var node = rIQ.getChild();
|
|
for (var i=0; i<payload.length; i++)
|
|
if(typeof payload[i] == 'string')
|
|
node.appendChild(rIQ.getDoc().loadXML(payload[i]));
|
|
else if (typeof payload[i] == 'object')
|
|
node.appendChild(payload[i]);
|
|
}
|
|
else if (typeof payload == 'object')
|
|
rIQ.getChild().appendChild(payload);
|
|
}
|
|
return rIQ;
|
|
};
|
|
|
|
/**
|
|
* A jabber/XMPP message packet
|
|
* @class Models the XMPP notion of an 'message' packet
|
|
* @extends JSJaCPacket
|
|
*/
|
|
function JSJaCMessage() {
|
|
/**
|
|
* @ignore
|
|
*/
|
|
this.base = JSJaCPacket;
|
|
this.base('message');
|
|
}
|
|
JSJaCMessage.prototype = new JSJaCPacket;
|
|
|
|
/**
|
|
* Sets the body of the message
|
|
* @param {String} body Your message to be sent along
|
|
* @return this message
|
|
* @type JSJaCMessage
|
|
*/
|
|
JSJaCMessage.prototype.setBody = function(body) {
|
|
this._setChildNode("body",body);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the subject of the message
|
|
* @param {String} subject Your subject to be sent along
|
|
* @return this message
|
|
* @type JSJaCMessage
|
|
*/
|
|
JSJaCMessage.prototype.setSubject = function(subject) {
|
|
this._setChildNode("subject",subject);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the 'tread' attribute for this message. This is used to identify
|
|
* threads in chat conversations
|
|
* @param {String} thread Usually a somewhat random hash.
|
|
* @return this message
|
|
* @type JSJaCMessage
|
|
*/
|
|
JSJaCMessage.prototype.setThread = function(thread) {
|
|
this._setChildNode("thread", thread);
|
|
return this;
|
|
};
|
|
/**
|
|
* Sets the 'nick' attribute for this message.
|
|
* This is sometime sused to detect the sender nickname when he's not in the roster
|
|
* @param {String} nickname
|
|
* @return this message
|
|
* @type JSJaCMessage
|
|
*/
|
|
JSJaCMessage.prototype.setNick = function(nick) {
|
|
var aNode = this.getChild("nick");
|
|
var tNode = this.getDoc().createTextNode(nick);
|
|
if (aNode)
|
|
try {
|
|
aNode.replaceChild(tNode,aNode.firstChild);
|
|
} catch (e) { }
|
|
else {
|
|
try {
|
|
aNode = this.getDoc().createElementNS('http://jabber.org/protocol/nick',
|
|
"nick");
|
|
} catch (ex) {
|
|
aNode = this.getDoc().createElement("nick")
|
|
}
|
|
this.getNode().appendChild(aNode);
|
|
aNode.appendChild(tNode);
|
|
}
|
|
return this;
|
|
};
|
|
/**
|
|
* Gets the 'thread' identifier for this message
|
|
* @return A thread identifier
|
|
* @type String
|
|
*/
|
|
JSJaCMessage.prototype.getThread = function() {
|
|
return this.getChildVal('thread');
|
|
};
|
|
/**
|
|
* Gets the body of this message
|
|
* @return The body of this message
|
|
* @type String
|
|
*/
|
|
JSJaCMessage.prototype.getBody = function() {
|
|
return this.getChildVal('body');
|
|
};
|
|
/**
|
|
* Gets the subject of this message
|
|
* @return The subject of this message
|
|
* @type String
|
|
*/
|
|
JSJaCMessage.prototype.getSubject = function() {
|
|
return this.getChildVal('subject')
|
|
};
|
|
/**
|
|
* Gets the nickname of this message
|
|
* @return The nickname of this message
|
|
* @type String
|
|
*/
|
|
JSJaCMessage.prototype.getNick = function() {
|
|
return this.getChildVal('nick');
|
|
};
|
|
|
|
|
|
/**
|
|
* Tries to transform a w3c DOM node to JSJaC's internal representation
|
|
* (JSJaCPacket type, one of JSJaCPresence, JSJaCMessage, JSJaCIQ)
|
|
* @param: {Node
|
|
* http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247}
|
|
* node The node to be transformed
|
|
* @return A JSJaCPacket representing the given node. If node's root
|
|
* elemenent is not one of 'message', 'presence' or 'iq',
|
|
* <code>null</code> is being returned.
|
|
* @type JSJaCPacket
|
|
*/
|
|
JSJaCPacket.wrapNode = function(node) {
|
|
var oPacket = null;
|
|
|
|
switch (node.nodeName.toLowerCase()) {
|
|
case 'presence':
|
|
oPacket = new JSJaCPresence();
|
|
break;
|
|
case 'message':
|
|
oPacket = new JSJaCMessage();
|
|
break;
|
|
case 'iq':
|
|
oPacket = new JSJaCIQ();
|
|
break;
|
|
}
|
|
|
|
if (oPacket) {
|
|
oPacket.getDoc().replaceChild(oPacket._importNode(node, true),
|
|
oPacket.getNode());
|
|
}
|
|
|
|
return oPacket;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* an error packet for internal use
|
|
* @private
|
|
* @constructor
|
|
*/
|
|
function JSJaCError(code,type,condition) {
|
|
var xmldoc = XmlDocument.create("error","jsjac");
|
|
|
|
xmldoc.documentElement.setAttribute('code',code);
|
|
xmldoc.documentElement.setAttribute('type',type);
|
|
if (condition)
|
|
xmldoc.documentElement.appendChild(xmldoc.createElement(condition)).
|
|
setAttribute('xmlns','urn:ietf:params:xml:ns:xmpp-stanzas');
|
|
return xmldoc.documentElement;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Creates a new set of hash keys
|
|
* @class Reflects a set of sha1/md5 hash keys for securing sessions
|
|
* @constructor
|
|
* @param {Function} func The hash function to be used for creating the keys
|
|
* @param {Debugger} oDbg Reference to debugger implementation [optional]
|
|
*/
|
|
function JSJaCKeys(func,oDbg) {
|
|
var seed = Math.random();
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this._k = new Array();
|
|
this._k[0] = seed.toString();
|
|
if (oDbg)
|
|
/**
|
|
* Reference to Debugger
|
|
* @type Debugger
|
|
*/
|
|
this.oDbg = oDbg;
|
|
else {
|
|
this.oDbg = {};
|
|
this.oDbg.log = function() {};
|
|
}
|
|
|
|
if (func) {
|
|
for (var i=1; i<JSJAC_NKEYS; i++) {
|
|
this._k[i] = func(this._k[i-1]);
|
|
oDbg.log(i+": "+this._k[i],4);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this._indexAt = JSJAC_NKEYS-1;
|
|
/**
|
|
* Gets next key from stack
|
|
* @return New hash key
|
|
* @type String
|
|
*/
|
|
this.getKey = function() {
|
|
return this._k[this._indexAt--];
|
|
};
|
|
/**
|
|
* Indicates whether there's only one key left
|
|
* @return <code>true</code> if there's only one key left, false otherwise
|
|
* @type boolean
|
|
*/
|
|
this.lastKey = function() { return (this._indexAt == 0); };
|
|
/**
|
|
* Returns number of overall/initial stack size
|
|
* @return Number of keys created
|
|
* @type int
|
|
*/
|
|
this.size = function() { return this._k.length; };
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this._getSuspendVars = function() {
|
|
return ('_k,_indexAt').split(',');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @fileoverview Contains all things in common for all subtypes of connections
|
|
* supported.
|
|
* @author Stefan Strigler steve@zeank.in-berlin.de
|
|
* @version $Revision$
|
|
*/
|
|
|
|
/**
|
|
* Creates a new Jabber connection (a connection to a jabber server)
|
|
* @class Somewhat abstract base class for jabber connections. Contains all
|
|
* of the code in common for all jabber connections
|
|
* @constructor
|
|
* @param {JSON http://www.json.org/index} oArg JSON with properties: <br>
|
|
* * <code>httpbase</code> the http base address of the service to be used for
|
|
* connecting to jabber<br>
|
|
* * <code>oDbg</code> (optional) a reference to a debugger interface
|
|
*/
|
|
function JSJaCConnection(oArg) {
|
|
|
|
if (oArg && oArg.oDbg && oArg.oDbg.log) {
|
|
/**
|
|
* Reference to debugger interface
|
|
* (needs to implement method <code>log</code>)
|
|
* @type Debugger
|
|
*/
|
|
this.oDbg = oArg.oDbg;
|
|
} else {
|
|
this.oDbg = new Object(); // always initialise a debugger
|
|
this.oDbg.log = function() { };
|
|
}
|
|
|
|
if (oArg && oArg.timerval)
|
|
this.setPollInterval(oArg.timerval);
|
|
else
|
|
this.setPollInterval(JSJAC_TIMERVAL);
|
|
|
|
if (oArg && oArg.httpbase)
|
|
/**
|
|
* @private
|
|
*/
|
|
this._httpbase = oArg.httpbase;
|
|
|
|
if (oArg &&oArg.allow_plain)
|
|
/**
|
|
* @private
|
|
*/
|
|
this.allow_plain = oArg.allow_plain;
|
|
else
|
|
this.allow_plain = JSJAC_ALLOW_PLAIN;
|
|
|
|
if (oArg && oArg.cookie_prefix)
|
|
/**
|
|
* @private
|
|
*/
|
|
this._cookie_prefix = oArg.cookie_prefix;
|
|
else
|
|
this._cookie_prefix = "";
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this._connected = false;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._events = new Array();
|
|
/**
|
|
* @private
|
|
*/
|
|
this._keys = null;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._ID = 0;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._inQ = new Array();
|
|
/**
|
|
* @private
|
|
*/
|
|
this._pQueue = new Array();
|
|
/**
|
|
* @private
|
|
*/
|
|
this._regIDs = new Array();
|
|
/**
|
|
* @private
|
|
*/
|
|
this._req = new Array();
|
|
/**
|
|
* @private
|
|
*/
|
|
this._status = 'intialized';
|
|
/**
|
|
* @private
|
|
*/
|
|
this._errcnt = 0;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._inactivity = JSJAC_INACTIVITY;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._sendRawCallbacks = new Array();
|
|
}
|
|
|
|
// Generates an ID
|
|
var STANZA_ID = 1;
|
|
|
|
function genID() {
|
|
return STANZA_ID++;
|
|
}
|
|
|
|
JSJaCConnection.prototype.connect = function(oArg) {
|
|
this._setStatus('connecting');
|
|
|
|
this.domain = oArg.domain || 'localhost';
|
|
this.username = oArg.username;
|
|
this.resource = oArg.resource;
|
|
this.pass = oArg.pass;
|
|
this.register = oArg.register;
|
|
|
|
this.authhost = oArg.authhost || this.domain;
|
|
this.authtype = oArg.authtype || 'sasl';
|
|
|
|
if (oArg.xmllang && oArg.xmllang != '')
|
|
this._xmllang = oArg.xmllang;
|
|
else
|
|
this._xmllang = 'en';
|
|
|
|
this.host = oArg.host || this.domain;
|
|
this.port = oArg.port || 5222;
|
|
if (oArg.secure)
|
|
this.secure = 'true';
|
|
else
|
|
this.secure = 'false';
|
|
|
|
if (oArg.wait)
|
|
this._wait = oArg.wait;
|
|
|
|
this.jid = this.username + '@' + this.domain;
|
|
this.fulljid = this.jid + '/' + this.resource;
|
|
|
|
this._rid = Math.round( 100000.5 + ( ( (900000.49999) - (100000.5) ) * Math.random() ) );
|
|
|
|
// setupRequest must be done after rid is created but before first use in reqstr
|
|
var slot = this._getFreeSlot();
|
|
this._req[slot] = this._setupRequest(true);
|
|
|
|
var reqstr = this._getInitialRequestString();
|
|
|
|
this.oDbg.log(reqstr,4);
|
|
|
|
this._req[slot].r.onreadystatechange =
|
|
JSJaC.bind(function() {
|
|
var r = this._req[slot].r;
|
|
if (r.readyState == 4) {
|
|
this.oDbg.log("async recv: "+r.responseText,4);
|
|
this._handleInitialResponse(r); // handle response
|
|
}
|
|
}, this);
|
|
|
|
if (typeof(this._req[slot].r.onerror) != 'undefined') {
|
|
this._req[slot].r.onerror =
|
|
JSJaC.bind(function(e) {
|
|
this.oDbg.log('XmlHttpRequest error',1);
|
|
return false;
|
|
}, this);
|
|
}
|
|
|
|
this._req[slot].r.send(reqstr);
|
|
};
|
|
|
|
/**
|
|
* Tells whether this connection is connected
|
|
* @return <code>true</code> if this connections is connected,
|
|
* <code>false</code> otherwise
|
|
* @type boolean
|
|
*/
|
|
JSJaCConnection.prototype.connected = function() { return this._connected; };
|
|
|
|
/**
|
|
* Disconnects from jabber server and terminates session (if applicable)
|
|
*/
|
|
JSJaCConnection.prototype.disconnect = function() {
|
|
this._setStatus('disconnecting');
|
|
|
|
if (!this.connected())
|
|
return;
|
|
this._connected = false;
|
|
|
|
clearInterval(this._interval);
|
|
clearInterval(this._inQto);
|
|
|
|
if (this._timeout)
|
|
clearTimeout(this._timeout); // remove timer
|
|
|
|
var slot = this._getFreeSlot();
|
|
// Intentionally synchronous
|
|
this._req[slot] = this._setupRequest(false);
|
|
|
|
request = this._getRequestString(false, true);
|
|
|
|
this.oDbg.log("Disconnecting: " + request,4);
|
|
this._req[slot].r.send(request);
|
|
|
|
try {
|
|
removeDB('jsjac', 'state');
|
|
} catch (e) {}
|
|
|
|
this.oDbg.log("Disconnected: "+this._req[slot].r.responseText,2);
|
|
this._handleEvent('ondisconnect');
|
|
};
|
|
|
|
/**
|
|
* Gets current value of polling interval
|
|
* @return Polling interval in milliseconds
|
|
* @type int
|
|
*/
|
|
JSJaCConnection.prototype.getPollInterval = function() {
|
|
return this._timerval;
|
|
};
|
|
|
|
/**
|
|
* Registers an event handler (callback) for this connection.
|
|
|
|
* <p>Note: All of the packet handlers for specific packets (like
|
|
* message_in, presence_in and iq_in) fire only if there's no
|
|
* callback associated with the id.<br>
|
|
|
|
* <p>Example:<br/>
|
|
* <code>con.registerHandler('iq', 'query', 'jabber:iq:version', handleIqVersion);</code>
|
|
|
|
|
|
* @param {String} event One of
|
|
|
|
* <ul>
|
|
* <li>onConnect - connection has been established and authenticated</li>
|
|
* <li>onDisconnect - connection has been disconnected</li>
|
|
* <li>onResume - connection has been resumed</li>
|
|
|
|
* <li>onStatusChanged - connection status has changed, current
|
|
* status as being passed argument to handler. See {@link #status}.</li>
|
|
|
|
* <li>onError - an error has occured, error node is supplied as
|
|
* argument, like this:<br><code><error code='404' type='cancel'><br>
|
|
* <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/><br>
|
|
* </error></code></li>
|
|
|
|
* <li>packet_in - a packet has been received (argument: the
|
|
* packet)</li>
|
|
|
|
* <li>packet_out - a packet is to be sent(argument: the
|
|
* packet)</li>
|
|
|
|
* <li>message_in | message - a message has been received (argument:
|
|
* the packet)</li>
|
|
|
|
* <li>message_out - a message packet is to be sent (argument: the
|
|
* packet)</li>
|
|
|
|
* <li>presence_in | presence - a presence has been received
|
|
* (argument: the packet)</li>
|
|
|
|
* <li>presence_out - a presence packet is to be sent (argument: the
|
|
* packet)</li>
|
|
|
|
* <li>iq_in | iq - an iq has been received (argument: the packet)</li>
|
|
* <li>iq_out - an iq is to be sent (argument: the packet)</li>
|
|
* </ul>
|
|
|
|
* @param {String} childName A childnode's name that must occur within a
|
|
* retrieved packet [optional]
|
|
|
|
* @param {String} childNS A childnode's namespace that must occure within
|
|
* a retrieved packet (works only if childName is given) [optional]
|
|
|
|
* @param {String} type The type of the packet to handle (works only if childName and chidNS are given (both may be set to '*' in order to get skipped) [optional]
|
|
|
|
* @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.
|
|
*/
|
|
JSJaCConnection.prototype.registerHandler = function(event) {
|
|
event = event.toLowerCase(); // don't be case-sensitive here
|
|
var eArg = {handler: arguments[arguments.length-1],
|
|
childName: '*',
|
|
childNS: '*',
|
|
type: '*'};
|
|
if (arguments.length > 2)
|
|
eArg.childName = arguments[1];
|
|
if (arguments.length > 3)
|
|
eArg.childNS = arguments[2];
|
|
if (arguments.length > 4)
|
|
eArg.type = arguments[3];
|
|
if (!this._events[event])
|
|
this._events[event] = new Array(eArg);
|
|
else
|
|
this._events[event] = this._events[event].concat(eArg);
|
|
|
|
// sort events in order how specific they match criterias thus using
|
|
// wildcard patterns puts them back in queue when it comes to
|
|
// bubbling the event
|
|
this._events[event] =
|
|
this._events[event].sort(function(a,b) {
|
|
var aRank = 0;
|
|
var bRank = 0;
|
|
with (a) {
|
|
if (type == '*')
|
|
aRank++;
|
|
if (childNS == '*')
|
|
aRank++;
|
|
if (childName == '*')
|
|
aRank++;
|
|
}
|
|
with (b) {
|
|
if (type == '*')
|
|
bRank++;
|
|
if (childNS == '*')
|
|
bRank++;
|
|
if (childName == '*')
|
|
bRank++;
|
|
}
|
|
if (aRank > bRank)
|
|
return 1;
|
|
if (aRank < bRank)
|
|
return -1;
|
|
return 0;
|
|
});
|
|
this.oDbg.log("registered handler for event '"+event+"'",2);
|
|
};
|
|
|
|
JSJaCConnection.prototype.unregisterHandler = function(event,handler) {
|
|
event = event.toLowerCase(); // don't be case-sensitive here
|
|
|
|
if (!this._events[event])
|
|
return;
|
|
|
|
var arr = this._events[event], res = new Array();
|
|
for (var i=0; i<arr.length; i++)
|
|
if (arr[i].handler != handler)
|
|
res.push(arr[i]);
|
|
|
|
if (arr.length != res.length) {
|
|
this._events[event] = res;
|
|
this.oDbg.log("unregistered handler for event '"+event+"'",2);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Register for iq packets of type 'get'.
|
|
* @param {String} childName A childnode's name that must occur within a
|
|
* retrieved packet
|
|
|
|
* @param {String} childNS A childnode's namespace that must occure within
|
|
* a retrieved packet (works only if childName is given)
|
|
|
|
* @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.
|
|
*/
|
|
JSJaCConnection.prototype.registerIQGet = function(childName, childNS, handler) {
|
|
this.registerHandler('iq', childName, childNS, 'get', handler);
|
|
};
|
|
|
|
/**
|
|
* Register for iq packets of type 'set'.
|
|
* @param {String} childName A childnode's name that must occur within a
|
|
* retrieved packet
|
|
|
|
* @param {String} childNS A childnode's namespace that must occure within
|
|
* a retrieved packet (works only if childName is given)
|
|
|
|
* @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.
|
|
*/
|
|
JSJaCConnection.prototype.registerIQSet = function(childName, childNS, handler) {
|
|
this.registerHandler('iq', childName, childNS, 'set', handler);
|
|
};
|
|
|
|
/**
|
|
* Resumes this connection from saved state (cookie)
|
|
* @return Whether resume was successful
|
|
* @type boolean
|
|
*/
|
|
JSJaCConnection.prototype.resume = function() {
|
|
try {
|
|
var json = getDB('jsjac', 'state');
|
|
this.oDbg.log('read cookie: '+json,2);
|
|
removeDB('jsjac', 'state');
|
|
|
|
return this.resumeFromData(JSJaCJSON.parse(json));
|
|
} catch (e) {}
|
|
return false; // sth went wrong
|
|
};
|
|
|
|
/**
|
|
* Resumes BOSH connection from data
|
|
* @param {Object} serialized jsjac state information
|
|
* @return Whether resume was successful
|
|
* @type boolean
|
|
*/
|
|
JSJaCConnection.prototype.resumeFromData = function(data) {
|
|
try {
|
|
this._setStatus('resuming');
|
|
|
|
for (var i in data)
|
|
if (data.hasOwnProperty(i))
|
|
this[i] = data[i];
|
|
|
|
// copy keys - not being very generic here :-/
|
|
if (this._keys) {
|
|
this._keys2 = new JSJaCKeys();
|
|
var u = this._keys2._getSuspendVars();
|
|
for (var i=0; i<u.length; i++)
|
|
this._keys2[u[i]] = this._keys[u[i]];
|
|
this._keys = this._keys2;
|
|
}
|
|
|
|
if (this._connected) {
|
|
// don't poll too fast!
|
|
this._handleEvent('onresume');
|
|
setTimeout(JSJaC.bind(this._resume, this),this.getPollInterval());
|
|
this._interval = setInterval(JSJaC.bind(this._checkQueue, this),
|
|
JSJAC_CHECKQUEUEINTERVAL);
|
|
this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
|
|
JSJAC_CHECKINQUEUEINTERVAL);
|
|
}
|
|
|
|
return (this._connected === true);
|
|
} catch (e) {
|
|
if (e.message)
|
|
this.oDbg.log("Resume failed: "+e.message, 1);
|
|
else
|
|
this.oDbg.log("Resume failed: "+e, 1);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sends a JSJaCPacket
|
|
* @param {JSJaCPacket} packet The packet to send
|
|
* @param {Function} cb The callback to be called if there's a reply
|
|
* to this packet (identified by id) [optional]
|
|
* @param {Object} arg Arguments passed to the callback
|
|
* (additionally to the packet received) [optional]
|
|
* @return 'true' if sending was successfull, 'false' otherwise
|
|
* @type boolean
|
|
*/
|
|
JSJaCConnection.prototype.send = function(packet,cb,arg) {
|
|
if (!packet || !packet.pType) {
|
|
this.oDbg.log("no packet: "+packet, 1);
|
|
return false;
|
|
}
|
|
|
|
if (!this.connected())
|
|
return false;
|
|
|
|
// generate an ID for the packet
|
|
if (!packet.getID())
|
|
packet.setID(genID());
|
|
|
|
// packet xml:lang
|
|
if (!packet.getXMLLang())
|
|
packet.setXMLLang(XML_LANG);
|
|
|
|
// remember id for response if callback present
|
|
if (cb)
|
|
this._registerPID(packet.getID(),cb,arg);
|
|
|
|
try {
|
|
this._handleEvent(packet.pType()+'_out', packet);
|
|
this._handleEvent("packet_out", packet);
|
|
this._pQueue = this._pQueue.concat(packet.xml());
|
|
} catch (e) {
|
|
this.oDbg.log(e.toString(),1);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Sends an IQ packet. Has default handlers for each reply type.
|
|
* Those maybe overriden by passing an appropriate handler.
|
|
* @param {JSJaCIQPacket} iq - the iq packet to send
|
|
* @param {Object} handlers - object with properties 'error_handler',
|
|
* 'result_handler' and 'default_handler'
|
|
* with appropriate functions
|
|
* @param {Object} arg - argument to handlers
|
|
* @return 'true' if sending was successfull, 'false' otherwise
|
|
* @type boolean
|
|
*/
|
|
JSJaCConnection.prototype.sendIQ = function(iq, handlers, arg) {
|
|
if (!iq || iq.pType() != 'iq') {
|
|
return false;
|
|
}
|
|
|
|
handlers = handlers || {};
|
|
var error_handler = handlers.error_handler || JSJaC.bind(function(aIq) {
|
|
this.oDbg.log(aIq.xml(), 1);
|
|
}, this);
|
|
|
|
var result_handler = handlers.result_handler || JSJaC.bind(function(aIq) {
|
|
this.oDbg.log(aIq.xml(), 2);
|
|
}, this);
|
|
|
|
var iqHandler = function(aIq, arg) {
|
|
switch (aIq.getType()) {
|
|
case 'error':
|
|
error_handler(aIq);
|
|
break;
|
|
case 'result':
|
|
result_handler(aIq, arg);
|
|
break;
|
|
}
|
|
};
|
|
return this.send(iq, iqHandler, arg);
|
|
};
|
|
|
|
/**
|
|
* Sets polling interval for this connection
|
|
* @param {int} millisecs Milliseconds to set timer to
|
|
* @return effective interval this connection has been set to
|
|
* @type int
|
|
*/
|
|
JSJaCConnection.prototype.setPollInterval = function(timerval) {
|
|
if (timerval && !isNaN(timerval))
|
|
this._timerval = timerval;
|
|
return this._timerval;
|
|
};
|
|
|
|
/**
|
|
* Returns current status of this connection
|
|
* @return String to denote current state. One of
|
|
* <ul>
|
|
* <li>'initializing' ... well
|
|
* <li>'connecting' if connect() was called
|
|
* <li>'resuming' if resume() was called
|
|
* <li>'processing' if it's about to operate as normal
|
|
* <li>'onerror_fallback' if there was an error with the request object
|
|
* <li>'protoerror_fallback' if there was an error at the http binding protocol flow (most likely that's where you interested in)
|
|
* <li>'internal_server_error' in case of an internal server error
|
|
* <li>'suspending' if suspend() is being called
|
|
* <li>'aborted' if abort() was called
|
|
* <li>'disconnecting' if disconnect() has been called
|
|
* </ul>
|
|
* @type String
|
|
*/
|
|
JSJaCConnection.prototype.status = function() { return this._status; };
|
|
|
|
/**
|
|
* Suspends this connection (saving state for later resume)
|
|
* Saves state to cookie
|
|
* @return Whether suspend (saving to cookie) was successful
|
|
* @type boolean
|
|
*/
|
|
JSJaCConnection.prototype.suspend = function(has_pause) {
|
|
var data = this.suspendToData(has_pause);
|
|
|
|
try {
|
|
var c = setDB('jsjac', 'state', JSJaCJSON.toString(data));
|
|
return c;
|
|
} catch (e) {
|
|
this.oDbg.log("Failed creating cookie '"+this._cookie_prefix+
|
|
"JSJaC_State': "+e.message,1);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Suspend connection and return serialized JSJaC connection state
|
|
* @return JSJaC connection state object
|
|
* @type Object
|
|
*/
|
|
JSJaCConnection.prototype.suspendToData = function(has_pause) {
|
|
|
|
// remove timers
|
|
if(has_pause) {
|
|
clearTimeout(this._timeout);
|
|
clearInterval(this._interval);
|
|
clearInterval(this._inQto);
|
|
|
|
this._suspend();
|
|
}
|
|
|
|
var u = ('_connected,_keys,_ID,_inQ,_pQueue,_regIDs,_errcnt,_inactivity,domain,username,resource,jid,fulljid,_sid,_httpbase,_timerval,_is_polling').split(',');
|
|
u = u.concat(this._getSuspendVars());
|
|
var s = new Object();
|
|
|
|
for (var i=0; i<u.length; i++) {
|
|
if (!this[u[i]]) continue; // hu? skip these!
|
|
if (this[u[i]]._getSuspendVars) {
|
|
var uo = this[u[i]]._getSuspendVars();
|
|
var o = new Object();
|
|
for (var j=0; j<uo.length; j++)
|
|
o[uo[j]] = this[u[i]][uo[j]];
|
|
} else
|
|
var o = this[u[i]];
|
|
|
|
s[u[i]] = o;
|
|
}
|
|
|
|
if(has_pause) {
|
|
this._connected = false;
|
|
this._setStatus('suspending');
|
|
}
|
|
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._abort = function() {
|
|
clearTimeout(this._timeout); // remove timer
|
|
|
|
clearInterval(this._inQto);
|
|
clearInterval(this._interval);
|
|
|
|
this._connected = false;
|
|
|
|
this._setStatus('aborted');
|
|
|
|
this.oDbg.log("Disconnected.",1);
|
|
this._handleEvent('ondisconnect');
|
|
this._handleEvent('onerror',
|
|
JSJaCError('500','cancel','service-unavailable'));
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._checkInQ = function() {
|
|
for (var i=0; i<this._inQ.length && i<10; i++) {
|
|
var item = this._inQ[0];
|
|
this._inQ = this._inQ.slice(1,this._inQ.length);
|
|
var packet = JSJaCPacket.wrapNode(item);
|
|
|
|
if (!packet)
|
|
return;
|
|
|
|
this._handleEvent("packet_in", packet);
|
|
|
|
if (packet.pType && !this._handlePID(packet)) {
|
|
this._handleEvent(packet.pType()+'_in',packet);
|
|
this._handleEvent(packet.pType(),packet);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._checkQueue = function() {
|
|
if (this._pQueue.length != 0)
|
|
this._process();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doAuth = function() {
|
|
if (this.has_sasl && this.authtype == 'nonsasl')
|
|
this.oDbg.log("Warning: SASL present but not used", 1);
|
|
|
|
if (!this._doSASLAuth() &&
|
|
!this._doLegacyAuth()) {
|
|
this.oDbg.log("Auth failed for authtype "+this.authtype,1);
|
|
this.disconnect();
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doInBandReg = function() {
|
|
if (this.authtype == 'saslanon' || this.authtype == 'anonymous')
|
|
return; // bullshit - no need to register if anonymous
|
|
|
|
/* ***
|
|
* In-Band Registration see JEP-0077
|
|
*/
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
iq.setID('reg1');
|
|
iq.appendNode("query", {xmlns: "jabber:iq:register"},
|
|
[["username", this.username],
|
|
["password", this.pass]]);
|
|
|
|
this.send(iq,this._doInBandRegDone);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doInBandRegDone = function(iq) {
|
|
if (iq && iq.getType() == 'error') { // we failed to register
|
|
this.oDbg.log("registration failed for "+this.username,0);
|
|
this._handleEvent('onerror',iq.getChild('error'));
|
|
return;
|
|
}
|
|
|
|
this.oDbg.log(this.username + " registered succesfully",0);
|
|
|
|
this._doAuth();
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doLegacyAuth = function() {
|
|
if (this.authtype != 'nonsasl' && this.authtype != 'anonymous')
|
|
return false;
|
|
|
|
/* ***
|
|
* Non-SASL Authentication as described in JEP-0078
|
|
*/
|
|
var iq = new JSJaCIQ();
|
|
iq.setIQ(null,'get','auth1');
|
|
iq.appendNode('query', {xmlns: 'jabber:iq:auth'},
|
|
[['username', this.username]]);
|
|
|
|
this.send(iq,this._doLegacyAuth2);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doLegacyAuth2 = function(iq) {
|
|
if (!iq || iq.getType() != 'result') {
|
|
if (iq && iq.getType() == 'error')
|
|
this._handleEvent('onerror',iq.getChild('error'));
|
|
this.disconnect();
|
|
return;
|
|
}
|
|
|
|
var use_digest = (iq.getChild('digest') != null);
|
|
|
|
/* ***
|
|
* Send authentication
|
|
*/
|
|
var iq = new JSJaCIQ();
|
|
iq.setIQ(null,'set','auth2');
|
|
|
|
query = iq.appendNode('query', {xmlns: 'jabber:iq:auth'},
|
|
[['username', this.username],
|
|
['resource', this.resource]]);
|
|
|
|
if (use_digest) { // digest login
|
|
query.appendChild(iq.buildNode('digest', {xmlns: 'jabber:iq:auth'},
|
|
hex_sha1(this.streamid + this.pass)));
|
|
} else if (this.allow_plain) { // use plaintext auth
|
|
query.appendChild(iq.buildNode('password', {xmlns: 'jabber:iq:auth'},
|
|
this.pass));
|
|
} else {
|
|
this.oDbg.log("no valid login mechanism found",1);
|
|
this.disconnect();
|
|
return false;
|
|
}
|
|
|
|
this.send(iq,this._doLegacyAuthDone);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doLegacyAuthDone = function(iq) {
|
|
if (iq.getType() != 'result') { // auth' failed
|
|
if (iq.getType() == 'error')
|
|
this._handleEvent('onerror',iq.getChild('error'));
|
|
this.disconnect();
|
|
} else
|
|
this._handleEvent('onconnect');
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doSASLAuth = function() {
|
|
if (this.authtype == 'nonsasl' || this.authtype == 'anonymous')
|
|
return false;
|
|
|
|
if (this.authtype == 'saslanon') {
|
|
if (this.mechs['ANONYMOUS']) {
|
|
this.oDbg.log("SASL using mechanism 'ANONYMOUS'",2);
|
|
return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>",
|
|
this._doSASLAuthDone);
|
|
}
|
|
this.oDbg.log("SASL ANONYMOUS requested but not supported",1);
|
|
} else {
|
|
if (this.mechs['DIGEST-MD5']) {
|
|
this.oDbg.log("SASL using mechanism 'DIGEST-MD5'",2);
|
|
return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>",
|
|
this._doSASLAuthDigestMd5S1);
|
|
} else if (this.allow_plain && this.mechs['PLAIN']) {
|
|
this.oDbg.log("SASL using mechanism 'PLAIN'",2);
|
|
var authStr = this.username+'@'+
|
|
this.domain+String.fromCharCode(0)+
|
|
this.username+String.fromCharCode(0)+
|
|
this.pass;
|
|
this.oDbg.log("authenticating with '"+authStr+"'",2);
|
|
authStr = b64encode(authStr);
|
|
return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"+authStr+"</auth>",
|
|
this._doSASLAuthDone);
|
|
}
|
|
this.oDbg.log("No SASL mechanism applied",1);
|
|
this.authtype = 'nonsasl'; // fallback
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doSASLAuthDigestMd5S1 = function(el) {
|
|
if (el.nodeName != "challenge") {
|
|
this.oDbg.log("challenge missing",1);
|
|
this._handleEvent('onerror',JSJaCError('401','auth','not-authorized'));
|
|
this.disconnect();
|
|
} else {
|
|
var challenge = b64decode(el.firstChild.nodeValue);
|
|
this.oDbg.log("got challenge: "+challenge,2);
|
|
this._nonce = challenge.substring(challenge.indexOf("nonce=")+7);
|
|
this._nonce = this._nonce.substring(0,this._nonce.indexOf("\""));
|
|
this.oDbg.log("nonce: "+this._nonce,2);
|
|
if (this._nonce == '' || this._nonce.indexOf('\"') != -1) {
|
|
this.oDbg.log("nonce not valid, aborting",1);
|
|
this.disconnect();
|
|
return;
|
|
}
|
|
|
|
this._digest_uri = "xmpp/";
|
|
// if (typeof(this.host) != 'undefined' && this.host != '') {
|
|
// this._digest-uri += this.host;
|
|
// if (typeof(this.port) != 'undefined' && this.port)
|
|
// this._digest-uri += ":" + this.port;
|
|
// this._digest-uri += '/';
|
|
// }
|
|
this._digest_uri += this.domain;
|
|
|
|
this._cnonce = cnonce(14);
|
|
|
|
this._nc = '00000001';
|
|
|
|
var X = this.username+':'+this.domain+':'+this.pass;
|
|
var Y = rstr_md5(str2rstr_utf8(X));
|
|
|
|
var A1 = Y+':'+this._nonce+':'+this._cnonce;
|
|
var HA1 = rstr2hex(rstr_md5(A1));
|
|
|
|
var A2 = 'AUTHENTICATE:'+this._digest_uri;
|
|
var HA2 = hex_md5(A2);
|
|
|
|
var response = hex_md5(HA1+':'+this._nonce+':'+this._nc+':'+
|
|
this._cnonce+':auth:'+HA2);
|
|
|
|
var rPlain = 'username="'+this.username+'",realm="'+this.domain+
|
|
'",nonce="'+this._nonce+'",cnonce="'+this._cnonce+'",nc="'+this._nc+
|
|
'",qop=auth,digest-uri="'+this._digest_uri+'",response="'+response+
|
|
'",charset="utf-8"';
|
|
|
|
this.oDbg.log("response: "+rPlain,2);
|
|
|
|
this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"+
|
|
b64encode(rPlain)+"</response>",
|
|
this._doSASLAuthDigestMd5S2);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doSASLAuthDigestMd5S2 = function(el) {
|
|
if (el.nodeName == 'failure') {
|
|
if (el.xml)
|
|
this.oDbg.log("auth error: "+el.xml,1);
|
|
else
|
|
this.oDbg.log("auth error",1);
|
|
this._handleEvent('onerror',JSJaCError('401','auth','not-authorized'));
|
|
this.disconnect();
|
|
return;
|
|
}
|
|
|
|
var response = b64decode(el.firstChild.nodeValue);
|
|
this.oDbg.log("response: "+response,2);
|
|
|
|
var rspauth = response.substring(response.indexOf("rspauth=")+8);
|
|
this.oDbg.log("rspauth: "+rspauth,2);
|
|
|
|
var X = this.username+':'+this.domain+':'+this.pass;
|
|
var Y = rstr_md5(str2rstr_utf8(X));
|
|
|
|
var A1 = Y+':'+this._nonce+':'+this._cnonce;
|
|
var HA1 = rstr2hex(rstr_md5(A1));
|
|
|
|
var A2 = ':'+this._digest_uri;
|
|
var HA2 = hex_md5(A2);
|
|
|
|
var rsptest = hex_md5(HA1+':'+this._nonce+':'+this._nc+':'+
|
|
this._cnonce+':auth:'+HA2);
|
|
this.oDbg.log("rsptest: "+rsptest,2);
|
|
|
|
if (rsptest != rspauth) {
|
|
this.oDbg.log("SASL Digest-MD5: server repsonse with wrong rspauth",1);
|
|
this.disconnect();
|
|
return;
|
|
}
|
|
|
|
if (el.nodeName == 'success') {
|
|
this._reInitStream(JSJaC.bind(this._doStreamBind, this));
|
|
} else { // some extra turn
|
|
this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>",
|
|
this._doSASLAuthDone);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doSASLAuthDone = function (el) {
|
|
if (el.nodeName != 'success') {
|
|
this.oDbg.log("auth failed",1);
|
|
this._handleEvent('onerror',JSJaCError('401','auth','not-authorized'));
|
|
this.disconnect();
|
|
} else {
|
|
this._reInitStream(JSJaC.bind(this._doStreamBind, this));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doStreamBind = function() {
|
|
var iq = new JSJaCIQ();
|
|
iq.setIQ(null,'set','bind_1');
|
|
iq.appendNode("bind", {xmlns: "urn:ietf:params:xml:ns:xmpp-bind"},
|
|
[["resource", this.resource]]);
|
|
this.oDbg.log(iq.xml());
|
|
this.send(iq,this._doXMPPSess);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doXMPPSess = function(iq) {
|
|
if (iq.getType() != 'result' || iq.getType() == 'error') { // failed
|
|
this.disconnect();
|
|
if (iq.getType() == 'error')
|
|
this._handleEvent('onerror',iq.getChild('error'));
|
|
return;
|
|
}
|
|
|
|
this.fulljid = iq.getChildVal("jid");
|
|
this.jid = this.fulljid.substring(0,this.fulljid.lastIndexOf('/'));
|
|
|
|
iq = new JSJaCIQ();
|
|
iq.setIQ(null,'set','sess_1');
|
|
iq.appendNode("session", {xmlns: "urn:ietf:params:xml:ns:xmpp-session"},
|
|
[]);
|
|
this.oDbg.log(iq.xml());
|
|
this.send(iq,this._doXMPPSessDone);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._doXMPPSessDone = function(iq) {
|
|
if (iq.getType() != 'result' || iq.getType() == 'error') { // failed
|
|
this.disconnect();
|
|
if (iq.getType() == 'error')
|
|
this._handleEvent('onerror',iq.getChild('error'));
|
|
return;
|
|
} else
|
|
this._handleEvent('onconnect');
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._handleEvent = function(event,arg) {
|
|
event = event.toLowerCase(); // don't be case-sensitive here
|
|
this.oDbg.log("incoming event '"+event+"'",3);
|
|
if (!this._events[event])
|
|
return;
|
|
this.oDbg.log("handling event '"+event+"'",2);
|
|
for (var i=0;i<this._events[event].length; i++) {
|
|
var aEvent = this._events[event][i];
|
|
if (typeof aEvent.handler == 'function') {
|
|
try {
|
|
if (arg) {
|
|
if (arg.pType) { // it's a packet
|
|
if ((!arg.getNode().hasChildNodes() && aEvent.childName != '*') ||
|
|
(arg.getNode().hasChildNodes() &&
|
|
!arg.getChild(aEvent.childName, aEvent.childNS)))
|
|
continue;
|
|
if (aEvent.type != '*' &&
|
|
arg.getType() != aEvent.type)
|
|
continue;
|
|
this.oDbg.log(aEvent.childName+"/"+aEvent.childNS+"/"+aEvent.type+" => match for handler "+aEvent.handler,3);
|
|
}
|
|
if (aEvent.handler(arg)) {
|
|
// handled!
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
if (aEvent.handler()) {
|
|
// handled!
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
|
|
if (e.fileName&&e.lineNumber) {
|
|
this.oDbg.log(aEvent.handler+"\n>>>"+e.name+": "+ e.message+' in '+e.fileName+' line '+e.lineNumber,1);
|
|
} else {
|
|
this.oDbg.log(aEvent.handler+"\n>>>"+e.name+": "+ e.message,1);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._handlePID = function(aJSJaCPacket) {
|
|
if (!aJSJaCPacket.getID())
|
|
return false;
|
|
for (var i in this._regIDs) {
|
|
if (this._regIDs.hasOwnProperty(i) &&
|
|
this._regIDs[i] && i == aJSJaCPacket.getID()) {
|
|
var pID = aJSJaCPacket.getID();
|
|
this.oDbg.log("handling "+pID,3);
|
|
try {
|
|
if (this._regIDs[i].cb.call(this, aJSJaCPacket, this._regIDs[i].arg) === false) {
|
|
// don't unregister
|
|
return false;
|
|
} else {
|
|
this._unregisterPID(pID);
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
// broken handler?
|
|
this.oDbg.log(e.name+": "+ e.message, 1);
|
|
this._unregisterPID(pID);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._handleResponse = function(req) {
|
|
var rootEl = this._parseResponse(req);
|
|
|
|
if (!rootEl)
|
|
return;
|
|
|
|
for (var i=0; i<rootEl.childNodes.length; i++) {
|
|
if (this._sendRawCallbacks.length) {
|
|
var cb = this._sendRawCallbacks[0];
|
|
this._sendRawCallbacks = this._sendRawCallbacks.slice(1, this._sendRawCallbacks.length);
|
|
cb.fn.call(this, rootEl.childNodes.item(i), cb.arg);
|
|
continue;
|
|
}
|
|
this._inQ = this._inQ.concat(rootEl.childNodes.item(i));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._parseStreamFeatures = function(doc) {
|
|
if (!doc) {
|
|
this.oDbg.log("nothing to parse ... aborting",1);
|
|
return false;
|
|
}
|
|
|
|
var errorTag;
|
|
if (doc.getElementsByTagNameNS) {
|
|
errorTag = doc.getElementsByTagNameNS("http://etherx.jabber.org/streams", "error").item(0);
|
|
} else {
|
|
var errors = doc.getElementsByTagName("error");
|
|
for (var i=0; i<errors.length; i++)
|
|
if (errors.item(i).namespaceURI == "http://etherx.jabber.org/streams" ||
|
|
errors.item(i).getAttribute('xmlns') == "http://etherx.jabber.org/streams") {
|
|
errorTag = errors.item(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (errorTag) {
|
|
this._setStatus("internal_server_error");
|
|
clearTimeout(this._timeout); // remove timer
|
|
clearInterval(this._interval);
|
|
clearInterval(this._inQto);
|
|
this._handleEvent('onerror',JSJaCError('503','cancel','session-terminate'));
|
|
this._connected = false;
|
|
this.oDbg.log("Disconnected.",1);
|
|
this._handleEvent('ondisconnect');
|
|
return false;
|
|
}
|
|
|
|
this.mechs = new Object();
|
|
var lMec1 = doc.getElementsByTagName("mechanisms");
|
|
this.has_sasl = false;
|
|
for (var i=0; i<lMec1.length; i++)
|
|
if (lMec1.item(i).getAttribute("xmlns") ==
|
|
"urn:ietf:params:xml:ns:xmpp-sasl") {
|
|
this.has_sasl=true;
|
|
var lMec2 = lMec1.item(i).getElementsByTagName("mechanism");
|
|
for (var j=0; j<lMec2.length; j++)
|
|
this.mechs[lMec2.item(j).firstChild.nodeValue] = true;
|
|
break;
|
|
}
|
|
if (this.has_sasl)
|
|
this.oDbg.log("SASL detected",2);
|
|
else {
|
|
this.oDbg.log("No support for SASL detected",2);
|
|
return false;
|
|
}
|
|
|
|
// Get the server CAPS (if available)
|
|
this.server_caps=null;
|
|
var sCaps = doc.getElementsByTagName("c");
|
|
for (var i=0; i<sCaps.length; i++) {
|
|
var c_sCaps=sCaps.item(i);
|
|
var x_sCaps=c_sCaps.getAttribute("xmlns");
|
|
var v_sCaps=c_sCaps.getAttribute("ver");
|
|
|
|
if ((x_sCaps == NS_CAPS) && v_sCaps) {
|
|
this.server_caps=v_sCaps;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._process = function(timerval) {
|
|
if (!this.connected()) {
|
|
this.oDbg.log("Connection lost ...",1);
|
|
if (this._interval)
|
|
clearInterval(this._interval);
|
|
return;
|
|
}
|
|
|
|
this.setPollInterval(timerval);
|
|
|
|
if (this._timeout)
|
|
clearTimeout(this._timeout);
|
|
|
|
var slot = this._getFreeSlot();
|
|
|
|
if (slot < 0)
|
|
return;
|
|
|
|
if (typeof(this._req[slot]) != 'undefined' &&
|
|
typeof(this._req[slot].r) != 'undefined' &&
|
|
this._req[slot].r.readyState != 4) {
|
|
this.oDbg.log("Slot "+slot+" is not ready");
|
|
return;
|
|
}
|
|
|
|
if (!this.isPolling() && this._pQueue.length == 0 &&
|
|
this._req[(slot+1)%2] && this._req[(slot+1)%2].r.readyState != 4) {
|
|
this.oDbg.log("all slots busy, standby ...", 2);
|
|
return;
|
|
}
|
|
|
|
if (!this.isPolling())
|
|
this.oDbg.log("Found working slot at "+slot,2);
|
|
|
|
this._req[slot] = this._setupRequest(true);
|
|
|
|
/* setup onload handler for async send */
|
|
this._req[slot].r.onreadystatechange =
|
|
JSJaC.bind(function() {
|
|
if (this._req[slot].r.readyState == 4) {
|
|
this._setStatus('processing');
|
|
this.oDbg.log("async recv: "+this._req[slot].r.responseText,4);
|
|
this._handleResponse(this._req[slot]);
|
|
|
|
if (!this.connected())
|
|
return;
|
|
|
|
// schedule next tick
|
|
if (this._pQueue.length) {
|
|
this._timeout = setTimeout(JSJaC.bind(this._process, this),100);
|
|
} else {
|
|
this.oDbg.log("scheduling next poll in "+this.getPollInterval()+
|
|
" msec", 4);
|
|
this._timeout = setTimeout(JSJaC.bind(this._process, this),this.getPollInterval());
|
|
}
|
|
}
|
|
}, this);
|
|
|
|
try {
|
|
this._req[slot].r.onerror =
|
|
JSJaC.bind(function() {
|
|
if (!this.connected())
|
|
return;
|
|
this._errcnt++;
|
|
this.oDbg.log('XmlHttpRequest error ('+this._errcnt+')',1);
|
|
if (this._errcnt > JSJAC_ERR_COUNT) {
|
|
// abort
|
|
this._abort();
|
|
return false;
|
|
}
|
|
|
|
this._setStatus('onerror_fallback');
|
|
|
|
// schedule next tick
|
|
setTimeout(JSJaC.bind(this._resume, this),this.getPollInterval());
|
|
return false;
|
|
}, this);
|
|
} catch(e) { } // well ... no onerror property available, maybe we
|
|
// can catch the error somewhere else ...
|
|
|
|
var reqstr = this._getRequestString();
|
|
|
|
if (typeof(this._rid) != 'undefined') // remember request id if any
|
|
this._req[slot].rid = this._rid;
|
|
|
|
this.oDbg.log("sending: " + reqstr,4);
|
|
this._req[slot].r.send(reqstr);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._registerPID = function(pID,cb,arg) {
|
|
if (!pID || !cb)
|
|
return false;
|
|
this._regIDs[pID] = new Object();
|
|
this._regIDs[pID].cb = cb;
|
|
if (arg)
|
|
this._regIDs[pID].arg = arg;
|
|
this.oDbg.log("registered "+pID,3);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* partial function binding sendEmpty to callback
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._prepSendEmpty = function(cb, ctx) {
|
|
return function() {
|
|
ctx._sendEmpty(JSJaC.bind(cb, ctx));
|
|
};
|
|
};
|
|
|
|
/**
|
|
* send empty request
|
|
* waiting for stream id to be able to proceed with authentication
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._sendEmpty = function(cb) {
|
|
var slot = this._getFreeSlot();
|
|
this._req[slot] = this._setupRequest(true);
|
|
|
|
this._req[slot].r.onreadystatechange =
|
|
JSJaC.bind(function() {
|
|
if (this._req[slot].r.readyState == 4) {
|
|
this.oDbg.log("async recv: "+this._req[slot].r.responseText,4);
|
|
cb(this._req[slot].r); // handle response
|
|
}
|
|
},this);
|
|
|
|
if (typeof(this._req[slot].r.onerror) != 'undefined') {
|
|
this._req[slot].r.onerror =
|
|
JSJaC.bind(function(e) {
|
|
this.oDbg.log('XmlHttpRequest error',1);
|
|
return false;
|
|
}, this);
|
|
}
|
|
|
|
var reqstr = this._getRequestString();
|
|
this.oDbg.log("sending: " + reqstr,4);
|
|
this._req[slot].r.send(reqstr);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._sendRaw = function(xml,cb,arg) {
|
|
if (cb)
|
|
this._sendRawCallbacks.push({fn: cb, arg: arg});
|
|
|
|
this._pQueue.push(xml);
|
|
this._process();
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._setStatus = function(status) {
|
|
if (!status || status == '')
|
|
return;
|
|
if (status != this._status) { // status changed!
|
|
this._status = status;
|
|
this._handleEvent('onstatuschanged', status);
|
|
this._handleEvent('status_changed', status);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCConnection.prototype._unregisterPID = function(pID) {
|
|
if (!this._regIDs[pID])
|
|
return false;
|
|
this._regIDs[pID] = null;
|
|
this.oDbg.log("unregistered "+pID,3);
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* @fileoverview All stuff related to HTTP Binding
|
|
* @author Stefan Strigler steve@zeank.in-berlin.de
|
|
* @version $Revision$
|
|
*/
|
|
|
|
/**
|
|
* Instantiates an HTTP Binding session
|
|
* @class Implementation of {@link
|
|
* http://www.xmpp.org/extensions/xep-0206.html XMPP Over BOSH}
|
|
* formerly known as HTTP Binding.
|
|
* @extends JSJaCConnection
|
|
* @constructor
|
|
*/
|
|
function JSJaCHttpBindingConnection(oArg) {
|
|
/**
|
|
* @ignore
|
|
*/
|
|
this.base = JSJaCConnection;
|
|
this.base(oArg);
|
|
|
|
// member vars
|
|
/**
|
|
* @private
|
|
*/
|
|
this._hold = JSJACHBC_MAX_HOLD;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._inactivity = 0;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._last_requests = new Object(); // 'hash' storing hold+1 last requests
|
|
/**
|
|
* @private
|
|
*/
|
|
this._last_rid = 0; // I know what you did last summer
|
|
/**
|
|
* @private
|
|
*/
|
|
this._min_polling = 0;
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this._pause = 0;
|
|
/**
|
|
* @private
|
|
*/
|
|
this._wait = JSJACHBC_MAX_WAIT;
|
|
}
|
|
JSJaCHttpBindingConnection.prototype = new JSJaCConnection();
|
|
|
|
/**
|
|
* Inherit an instantiated HTTP Binding session
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype.inherit = function(oArg) {
|
|
if (oArg.jid) {
|
|
var oJid = new JSJaCJID(oArg.jid);
|
|
this.domain = oJid.getDomain();
|
|
this.username = oJid.getNode();
|
|
this.resource = oJid.getResource();
|
|
} else {
|
|
this.domain = oArg.domain || 'localhost';
|
|
this.username = oArg.username;
|
|
this.resource = oArg.resource;
|
|
}
|
|
this._sid = oArg.sid;
|
|
this._rid = oArg.rid;
|
|
this._min_polling = oArg.polling;
|
|
this._inactivity = oArg.inactivity;
|
|
this._setHold(oArg.requests-1);
|
|
this.setPollInterval(this._timerval);
|
|
if (oArg.wait)
|
|
this._wait = oArg.wait; // for whatever reason
|
|
|
|
this._connected = true;
|
|
|
|
this._handleEvent('onconnect');
|
|
|
|
this._interval= setInterval(JSJaC.bind(this._checkQueue, this),
|
|
JSJAC_CHECKQUEUEINTERVAL);
|
|
this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
|
|
JSJAC_CHECKINQUEUEINTERVAL);
|
|
this._timeout = setTimeout(JSJaC.bind(this._process, this),
|
|
this.getPollInterval());
|
|
};
|
|
|
|
/**
|
|
* Sets poll interval
|
|
* @param {int} timerval the interval in seconds
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype.setPollInterval = function(timerval) {
|
|
if (timerval && !isNaN(timerval)) {
|
|
if (!this.isPolling())
|
|
this._timerval = 100;
|
|
else if (this._min_polling && timerval < this._min_polling*1000)
|
|
this._timerval = this._min_polling*1000;
|
|
else if (this._inactivity && timerval > this._inactivity*1000)
|
|
this._timerval = this._inactivity*1000;
|
|
else
|
|
this._timerval = timerval;
|
|
}
|
|
return this._timerval;
|
|
};
|
|
|
|
/**
|
|
* whether this session is in polling mode
|
|
* @type boolean
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype.isPolling = function() { return (this._hold == 0) };
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._getFreeSlot = function() {
|
|
for (var i=0; i<this._hold+1; i++)
|
|
if (typeof(this._req[i]) == 'undefined' || typeof(this._req[i].r) == 'undefined' || this._req[i].r.readyState == 4)
|
|
return i;
|
|
return -1; // nothing found
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._getHold = function() { return this._hold; };
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._getRequestString = function(raw, last) {
|
|
raw = raw || '';
|
|
var reqstr = '';
|
|
|
|
// check if we're repeating a request
|
|
|
|
if (this._rid <= this._last_rid && typeof(this._last_requests[this._rid]) != 'undefined') // repeat!
|
|
reqstr = this._last_requests[this._rid].xml;
|
|
else { // grab from queue
|
|
var xml = '';
|
|
while (this._pQueue.length) {
|
|
var curNode = this._pQueue[0];
|
|
xml += curNode;
|
|
this._pQueue = this._pQueue.slice(1,this._pQueue.length);
|
|
}
|
|
|
|
reqstr = "<body xml:lang='"+XML_LANG+"' rid='"+this._rid+"' sid='"+this._sid+"' xmlns='http://jabber.org/protocol/httpbind' ";
|
|
if (JSJAC_HAVEKEYS) {
|
|
reqstr += "key='"+this._keys.getKey()+"' ";
|
|
if (this._keys.lastKey()) {
|
|
this._keys = new JSJaCKeys(hex_sha1,this.oDbg);
|
|
reqstr += "newkey='"+this._keys.getKey()+"' ";
|
|
}
|
|
}
|
|
if (last)
|
|
reqstr += "type='terminate'";
|
|
else if (this._reinit) {
|
|
if (JSJACHBC_USE_BOSH_VER)
|
|
reqstr += "xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh' to='"+this.domain+"'";
|
|
this._reinit = false;
|
|
}
|
|
|
|
if (xml != '' || raw != '') {
|
|
reqstr += ">" + raw + xml + "</body>";
|
|
} else {
|
|
reqstr += "/>";
|
|
}
|
|
|
|
this._last_requests[this._rid] = new Object();
|
|
this._last_requests[this._rid].xml = reqstr;
|
|
this._last_rid = this._rid;
|
|
|
|
for (var i in this._last_requests)
|
|
if (this._last_requests.hasOwnProperty(i) &&
|
|
i < this._rid-this._hold)
|
|
delete(this._last_requests[i]); // truncate
|
|
}
|
|
|
|
return reqstr;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._getInitialRequestString = function() {
|
|
var reqstr = "<body xml:lang='"+XML_LANG+"' content='text/xml; charset=utf-8' hold='"+this._hold+"' xmlns='http://jabber.org/protocol/httpbind' to='"+this.authhost+"' wait='"+this._wait+"' rid='"+this._rid+"'";
|
|
if (this.secure)
|
|
reqstr += " secure='"+this.secure+"'";
|
|
if (JSJAC_HAVEKEYS) {
|
|
this._keys = new JSJaCKeys(hex_sha1,this.oDbg); // generate first set of keys
|
|
key = this._keys.getKey();
|
|
reqstr += " newkey='"+key+"'";
|
|
}
|
|
|
|
if (JSJACHBC_USE_BOSH_VER) {
|
|
reqstr += " ver='" + JSJACHBC_BOSH_VERSION + "'";
|
|
reqstr += " xmlns:xmpp='urn:xmpp:xbosh'";
|
|
if (this.authtype == 'sasl' || this.authtype == 'saslanon')
|
|
reqstr += " xmpp:version='1.0'";
|
|
}
|
|
reqstr += "/>";
|
|
return reqstr;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._getStreamID = function(req) {
|
|
|
|
this.oDbg.log(req.responseText,4);
|
|
|
|
if (!req.responseXML || !req.responseXML.documentElement) {
|
|
this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
|
|
return;
|
|
}
|
|
var body = req.responseXML.documentElement;
|
|
|
|
// any session error?
|
|
if(body.getAttribute('type') == 'terminate') {
|
|
this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
|
|
return;
|
|
}
|
|
|
|
// extract stream id used for non-SASL authentication
|
|
if (body.getAttribute('authid')) {
|
|
this.streamid = body.getAttribute('authid');
|
|
this.oDbg.log("got streamid: "+this.streamid,2);
|
|
}
|
|
|
|
if (!this._parseStreamFeatures(body)) {
|
|
this._sendEmpty(JSJaC.bind(this._getStreamID, this));
|
|
return;
|
|
}
|
|
|
|
this._timeout = setTimeout(JSJaC.bind(this._process, this),
|
|
this.getPollInterval());
|
|
|
|
if (this.register)
|
|
this._doInBandReg();
|
|
else
|
|
this._doAuth();
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._getSuspendVars = function() {
|
|
return ('host,port,secure,_rid,_last_rid,_wait,_min_polling,_inactivity,_hold,_last_requests,_pause').split(',');
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._handleInitialResponse = function(req) {
|
|
try {
|
|
// This will throw an error on Mozilla when the connection was refused
|
|
this.oDbg.log(req.getAllResponseHeaders(),4);
|
|
this.oDbg.log(req.responseText,4);
|
|
} catch(ex) {
|
|
this.oDbg.log("No response",4);
|
|
}
|
|
|
|
if (req.status != 200 || !req.responseXML) {
|
|
this.oDbg.log("initial response broken (status: "+req.status+")",1);
|
|
this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
|
|
return;
|
|
}
|
|
var body = req.responseXML.documentElement;
|
|
|
|
if (!body || body.tagName != 'body' || body.namespaceURI != 'http://jabber.org/protocol/httpbind') {
|
|
this.oDbg.log("no body element or incorrect body in initial response",1);
|
|
this._handleEvent("onerror",JSJaCError("500","wait","internal-service-error"));
|
|
return;
|
|
}
|
|
|
|
// Check for errors from the server
|
|
if (body.getAttribute("type") == "terminate") {
|
|
this.oDbg.log("invalid response:\n" + req.responseText,1);
|
|
clearTimeout(this._timeout); // remove timer
|
|
this._connected = false;
|
|
this.oDbg.log("Disconnected.",1);
|
|
this._handleEvent('ondisconnect');
|
|
this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
|
|
return;
|
|
}
|
|
|
|
// get session ID
|
|
this._sid = body.getAttribute('sid');
|
|
this.oDbg.log("got sid: "+this._sid,2);
|
|
|
|
// get attributes from response body
|
|
if (body.getAttribute('polling'))
|
|
this._min_polling = body.getAttribute('polling');
|
|
|
|
if (body.getAttribute('inactivity'))
|
|
this._inactivity = body.getAttribute('inactivity');
|
|
|
|
if (body.getAttribute('requests'))
|
|
this._setHold(body.getAttribute('requests')-1);
|
|
this.oDbg.log("set hold to " + this._getHold(),2);
|
|
|
|
if (body.getAttribute('ver'))
|
|
this._bosh_version = body.getAttribute('ver');
|
|
|
|
if (body.getAttribute('maxpause'))
|
|
this._pause = Number.min(body.getAttribute('maxpause'), JSJACHBC_MAXPAUSE);
|
|
|
|
// must be done after response attributes have been collected
|
|
this.setPollInterval(this._timerval);
|
|
|
|
/* start sending from queue for not polling connections */
|
|
this._connected = true;
|
|
|
|
this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
|
|
JSJAC_CHECKINQUEUEINTERVAL);
|
|
this._interval= setInterval(JSJaC.bind(this._checkQueue, this),
|
|
JSJAC_CHECKQUEUEINTERVAL);
|
|
|
|
/* wait for initial stream response to extract streamid needed
|
|
* for digest auth
|
|
*/
|
|
this._getStreamID(req);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._parseResponse = function(req) {
|
|
if (!this.connected() || !req)
|
|
return null;
|
|
|
|
var r = req.r; // the XmlHttpRequest
|
|
|
|
try {
|
|
if (r.status == 404 || r.status == 403) {
|
|
// connection manager killed session
|
|
this._abort();
|
|
return null;
|
|
}
|
|
|
|
if (r.status != 200 || !r.responseXML) {
|
|
this._errcnt++;
|
|
var errmsg = "invalid response ("+r.status+"):\n" + r.getAllResponseHeaders()+"\n"+r.responseText;
|
|
if (!r.responseXML)
|
|
errmsg += "\nResponse failed to parse!";
|
|
this.oDbg.log(errmsg,1);
|
|
if (this._errcnt > JSJAC_ERR_COUNT) {
|
|
// abort
|
|
this._abort();
|
|
return null;
|
|
}
|
|
|
|
if (this.connected()) {
|
|
this.oDbg.log("repeating ("+this._errcnt+")",1);
|
|
this._setStatus('proto_error_fallback');
|
|
|
|
// schedule next tick
|
|
setTimeout(JSJaC.bind(this._resume, this),
|
|
this.getPollInterval());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
this.oDbg.log("XMLHttpRequest error: status not available", 1);
|
|
this._errcnt++;
|
|
if (this._errcnt > JSJAC_ERR_COUNT) {
|
|
// abort
|
|
this._abort();
|
|
} else {
|
|
if (this.connected()) {
|
|
this.oDbg.log("repeating ("+this._errcnt+")",1);
|
|
|
|
this._setStatus('proto_error_fallback');
|
|
|
|
// schedule next tick
|
|
setTimeout(JSJaC.bind(this._resume, this),
|
|
this.getPollInterval());
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
var body = r.responseXML.documentElement;
|
|
if (!body || body.tagName != 'body' ||
|
|
body.namespaceURI != 'http://jabber.org/protocol/httpbind') {
|
|
this.oDbg.log("invalid response:\n" + r.responseText,1);
|
|
|
|
clearTimeout(this._timeout); // remove timer
|
|
clearInterval(this._interval);
|
|
clearInterval(this._inQto);
|
|
|
|
this._connected = false;
|
|
this.oDbg.log("Disconnected.",1);
|
|
this._handleEvent('ondisconnect');
|
|
|
|
this._setStatus('internal_server_error');
|
|
this._handleEvent('onerror',
|
|
JSJaCError('500','wait','internal-server-error'));
|
|
|
|
return null;
|
|
}
|
|
|
|
if (typeof(req.rid) != 'undefined' && this._last_requests[req.rid]) {
|
|
if (this._last_requests[req.rid].handled) {
|
|
this.oDbg.log("already handled "+req.rid,2);
|
|
return null;
|
|
} else
|
|
this._last_requests[req.rid].handled = true;
|
|
}
|
|
|
|
|
|
// Check for errors from the server
|
|
if (body.getAttribute("type") == "terminate") {
|
|
// read condition
|
|
var condition = body.getAttribute('condition');
|
|
|
|
if (condition != "item-not-found") {
|
|
this.oDbg.log("session terminated:\n" + r.responseText,1);
|
|
|
|
clearTimeout(this._timeout); // remove timer
|
|
clearInterval(this._interval);
|
|
clearInterval(this._inQto);
|
|
|
|
try {
|
|
removeDB('jsjac', 'state');
|
|
} catch (e) {}
|
|
|
|
this._connected = false;
|
|
|
|
if (condition == "remote-stream-error")
|
|
if (body.getElementsByTagName("conflict").length > 0)
|
|
this._setStatus("session-terminate-conflict");
|
|
if (condition == null)
|
|
condition = 'session-terminate';
|
|
this._handleEvent('onerror',JSJaCError('503','cancel',condition));
|
|
|
|
this.oDbg.log("Aborting remaining connections",4);
|
|
|
|
for (var i=0; i<this._hold+1; i++) {
|
|
try {
|
|
this._req[i].r.abort();
|
|
} catch(e) { this.oDbg.log(e, 1); }
|
|
}
|
|
|
|
this.oDbg.log("parseResponse done with terminating", 3);
|
|
|
|
this.oDbg.log("Disconnected.",1);
|
|
this._handleEvent('ondisconnect');
|
|
} else {
|
|
this._errcnt++;
|
|
if (this._errcnt > JSJAC_ERR_COUNT)
|
|
this._abort();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// no error
|
|
this._errcnt = 0;
|
|
return r.responseXML.documentElement;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._reInitStream = function(cb) {
|
|
// tell http binding to reinit stream with/before next request
|
|
this._reinit = true;
|
|
|
|
this._sendEmpty(this._prepReInitStreamWait(cb));
|
|
};
|
|
|
|
|
|
JSJaCHttpBindingConnection.prototype._prepReInitStreamWait = function(cb) {
|
|
return JSJaC.bind(function(req) {
|
|
this._reInitStreamWait(req, cb);
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._reInitStreamWait = function(req, cb) {
|
|
this.oDbg.log("checking for stream features");
|
|
var doc = req.responseXML.documentElement;
|
|
this.oDbg.log(doc);
|
|
if (doc.getElementsByTagNameNS) {
|
|
this.oDbg.log("checking with namespace");
|
|
var features = doc.getElementsByTagNameNS('http://etherx.jabber.org/streams',
|
|
'features').item(0);
|
|
if (features) {
|
|
var bind = features.getElementsByTagNameNS('urn:ietf:params:xml:ns:xmpp-bind',
|
|
'bind').item(0);
|
|
}
|
|
} else {
|
|
var featuresNL = doc.getElementsByTagName('stream:features');
|
|
for (var i=0, l=featuresNL.length; i<l; i++) {
|
|
if (featuresNL.item(i).namespaceURI == 'http://etherx.jabber.org/streams' ||
|
|
featuresNL.item(i).getAttribute('xmlns') ==
|
|
'http://etherx.jabber.org/streams') {
|
|
var features = featuresNL.item(i);
|
|
break;
|
|
}
|
|
}
|
|
if (features) {
|
|
var bind = features.getElementsByTagName('bind');
|
|
for (var i=0, l=bind.length; i<l; i++) {
|
|
if (bind.item(i).namespaceURI == 'urn:ietf:params:xml:ns:xmpp-bind' ||
|
|
bind.item(i).getAttribute('xmlns') ==
|
|
'urn:ietf:params:xml:ns:xmpp-bind') {
|
|
bind = bind.item(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.oDbg.log(features);
|
|
this.oDbg.log(bind);
|
|
|
|
if (features) {
|
|
if (bind) {
|
|
cb();
|
|
} else {
|
|
this.oDbg.log("no bind feature - giving up",1);
|
|
this._handleEvent('onerror',JSJaCError('503','cancel',"service-unavailable"));
|
|
this._connected = false;
|
|
this.oDbg.log("Disconnected.",1);
|
|
this._handleEvent('ondisconnect');
|
|
}
|
|
} else {
|
|
// wait
|
|
this._sendEmpty(this._prepReInitStreamWait(cb));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._resume = function() {
|
|
/* make sure to repeat last request as we can be sure that
|
|
* it had failed (only if we're not using the 'pause' attribute
|
|
*/
|
|
if (this._pause == 0 && this._rid >= this._last_rid)
|
|
this._rid = this._last_rid-1;
|
|
|
|
this._process();
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._setHold = function(hold) {
|
|
if (!hold || isNaN(hold) || hold < 0)
|
|
hold = 0;
|
|
else if (hold > JSJACHBC_MAX_HOLD)
|
|
hold = JSJACHBC_MAX_HOLD;
|
|
this._hold = hold;
|
|
return this._hold;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._setupRequest = function(async) {
|
|
var req = new Object();
|
|
var r = XmlHttp.create();
|
|
try {
|
|
r.open("POST",this._httpbase,async);
|
|
r.setRequestHeader('Content-Type','text/xml; charset=utf-8');
|
|
} catch(e) { this.oDbg.log(e,1); }
|
|
req.r = r;
|
|
this._rid++;
|
|
req.rid = this._rid;
|
|
return req;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
JSJaCHttpBindingConnection.prototype._suspend = function() {
|
|
if (this._pause == 0)
|
|
return; // got nothing to do
|
|
|
|
var slot = this._getFreeSlot();
|
|
// Intentionally synchronous
|
|
this._req[slot] = this._setupRequest(false);
|
|
|
|
var reqstr = "<body xml:lang='"+XML_LANG+"' pause='"+this._pause+"' xmlns='http://jabber.org/protocol/httpbind' sid='"+this._sid+"' rid='"+this._rid+"'";
|
|
if (JSJAC_HAVEKEYS) {
|
|
reqstr += " key='"+this._keys.getKey()+"'";
|
|
if (this._keys.lastKey()) {
|
|
this._keys = new JSJaCKeys(hex_sha1,this.oDbg);
|
|
reqstr += " newkey='"+this._keys.getKey()+"'";
|
|
}
|
|
|
|
}
|
|
reqstr += ">";
|
|
|
|
while (this._pQueue.length) {
|
|
var curNode = this._pQueue[0];
|
|
reqstr += curNode;
|
|
this._pQueue = this._pQueue.slice(1,this._pQueue.length);
|
|
}
|
|
|
|
//reqstr += "<presence type='unavailable' xmlns='jabber:client'/>";
|
|
reqstr += "</body>";
|
|
|
|
this.oDbg.log("Disconnecting: " + reqstr,4);
|
|
this._req[slot].r.send(reqstr);
|
|
};
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the constants JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: dual-licensed under AGPL and MPLv2
|
|
Authors: Stefan Strigler, Valérian Saliou, Kloadut
|
|
Last revision: 07/06/13
|
|
|
|
*/
|
|
|
|
// XMPP XMLNS attributes
|
|
var NS_PROTOCOL = 'http://jabber.org/protocol/';
|
|
var NS_FEATURES = 'http://jabber.org/features/';
|
|
var NS_CLIENT = 'jabber:client';
|
|
var NS_IQ = 'jabber:iq:';
|
|
var NS_X = 'jabber:x:';
|
|
var NS_IETF = 'urn:ietf:params:xml:ns:xmpp-';
|
|
var NS_XMPP = 'urn:xmpp:';
|
|
|
|
var NS_STORAGE = 'storage:';
|
|
var NS_BOOKMARKS = NS_STORAGE + 'bookmarks';
|
|
var NS_ROSTERNOTES = NS_STORAGE + 'rosternotes';
|
|
|
|
var NS_JAPPIX = 'jappix:';
|
|
var NS_INBOX = NS_JAPPIX + 'inbox';
|
|
var NS_OPTIONS = NS_JAPPIX + 'options';
|
|
|
|
var NS_DISCO_ITEMS = NS_PROTOCOL + 'disco#items';
|
|
var NS_DISCO_INFO = NS_PROTOCOL + 'disco#info';
|
|
var NS_VCARD = 'vcard-temp';
|
|
var NS_VCARD_P = NS_VCARD + ':x:update';
|
|
var NS_AUTH = NS_IQ + 'auth';
|
|
var NS_AUTH_ERROR = NS_IQ + 'auth:error';
|
|
var NS_REGISTER = NS_IQ + 'register';
|
|
var NS_SEARCH = NS_IQ + 'search';
|
|
var NS_ROSTER = NS_IQ + 'roster';
|
|
var NS_PRIVACY = NS_IQ + 'privacy';
|
|
var NS_PRIVATE = NS_IQ + 'private';
|
|
var NS_VERSION = NS_IQ + 'version';
|
|
var NS_TIME = NS_IQ + 'time';
|
|
var NS_LAST = NS_IQ + 'last';
|
|
var NS_IQDATA = NS_IQ + 'data';
|
|
var NS_XDATA = NS_X + 'data';
|
|
var NS_IQOOB = NS_IQ + 'oob';
|
|
var NS_XOOB = NS_X + 'oob';
|
|
var NS_DELAY = NS_X + 'delay';
|
|
var NS_EXPIRE = NS_X + 'expire';
|
|
var NS_EVENT = NS_X + 'event';
|
|
var NS_XCONFERENCE = NS_X + 'conference';
|
|
var NS_STATS = NS_PROTOCOL + 'stats';
|
|
var NS_MUC = NS_PROTOCOL + 'muc';
|
|
var NS_MUC_USER = NS_MUC + '#user';
|
|
var NS_MUC_ADMIN = NS_MUC + '#admin';
|
|
var NS_MUC_OWNER = NS_MUC + '#owner';
|
|
var NS_MUC_CONFIG = NS_MUC + '#roomconfig';
|
|
var NS_PUBSUB = NS_PROTOCOL + 'pubsub';
|
|
var NS_PUBSUB_EVENT = NS_PUBSUB + '#event';
|
|
var NS_PUBSUB_OWNER = NS_PUBSUB + '#owner';
|
|
var NS_PUBSUB_NMI = NS_PUBSUB + '#node-meta-info';
|
|
var NS_PUBSUB_NC = NS_PUBSUB + '#node_config';
|
|
var NS_PUBSUB_CN = NS_PUBSUB + '#config-node';
|
|
var NS_PUBSUB_RI = NS_PUBSUB + '#retrieve-items';
|
|
var NS_COMMANDS = NS_PROTOCOL + 'commands';
|
|
var NS_BOSH = NS_PROTOCOL + 'httpbind';
|
|
var NS_STREAM = 'http://etherx.jabber.org/streams';
|
|
var NS_URN_TIME = NS_XMPP + 'time';
|
|
var NS_URN_PING = NS_XMPP + 'ping';
|
|
var NS_URN_ADATA = NS_XMPP + 'avatar:data';
|
|
var NS_URN_AMETA = NS_XMPP + 'avatar:metadata';
|
|
var NS_URN_MBLOG = NS_XMPP + 'microblog:0';
|
|
var NS_URN_INBOX = NS_XMPP + 'inbox';
|
|
var NS_URN_FORWARD = NS_XMPP + 'forward:0';
|
|
var NS_URN_MAM = NS_XMPP + 'mam:0';
|
|
var NS_URN_DELAY = NS_XMPP + 'delay';
|
|
var NS_URN_RECEIPTS = NS_XMPP + 'receipts';
|
|
var NS_RSM = NS_PROTOCOL + 'rsm';
|
|
var NS_IPV6 = 'ipv6';
|
|
var NS_XHTML = 'http://www.w3.org/1999/xhtml';
|
|
var NS_XHTML_IM = NS_PROTOCOL + 'xhtml-im';
|
|
var NS_CHATSTATES = NS_PROTOCOL + 'chatstates';
|
|
var NS_HTTP_AUTH = NS_PROTOCOL + 'http-auth';
|
|
var NS_ROSTERX = NS_PROTOCOL + 'rosterx';
|
|
var NS_MOOD = NS_PROTOCOL + 'mood';
|
|
var NS_ACTIVITY = NS_PROTOCOL + 'activity';
|
|
var NS_TUNE = NS_PROTOCOL + 'tune';
|
|
var NS_GEOLOC = NS_PROTOCOL + 'geoloc';
|
|
var NS_NICK = NS_PROTOCOL + 'nick';
|
|
var NS_NOTIFY = '+notify';
|
|
var NS_CAPS = NS_PROTOCOL + 'caps';
|
|
var NS_ATOM = 'http://www.w3.org/2005/Atom';
|
|
|
|
var NS_STANZAS = NS_IETF + 'stanzas';
|
|
var NS_STREAMS = NS_IETF + 'streams';
|
|
|
|
var NS_TLS = NS_IETF + 'tls';
|
|
var NS_SASL = NS_IETF + 'sasl';
|
|
var NS_SESSION = NS_IETF + 'session';
|
|
var NS_BIND = NS_IETF + 'bind';
|
|
|
|
var NS_FEATURE_IQAUTH = NS_FEATURES + 'iq-auth';
|
|
var NS_FEATURE_IQREGISTER = NS_FEATURES + 'iq-register';
|
|
var NS_FEATURE_COMPRESS = NS_FEATURES + 'compress';
|
|
|
|
var NS_COMPRESS = NS_PROTOCOL + 'compress';
|
|
|
|
// Available locales
|
|
var LOCALES_AVAILABLE_ID = new Array();
|
|
var LOCALES_AVAILABLE_NAMES = new Array();
|
|
|
|
// XML lang
|
|
var XML_LANG = null;
|
|
|
|
// Jappix parameters
|
|
var JAPPIX_STATIC = null;
|
|
var JAPPIX_VERSION = null;
|
|
var JAPPIX_MAX_FILE_SIZE = null;
|
|
var JAPPIX_MAX_UPLOAD = null;
|
|
|
|
// Jappix main configuration
|
|
var SERVICE_NAME = null;
|
|
var SERVICE_DESC = null;
|
|
var OWNER_NAME = null;
|
|
var OWNER_WEBSITE = null;
|
|
var LEGAL = null;
|
|
var JAPPIX_RESOURCE = null;
|
|
var LOCK_HOST = null;
|
|
var ANONYMOUS = null;
|
|
var HTTP_AUTH = null;
|
|
var REGISTRATION = null;
|
|
var BOSH_PROXY = null;
|
|
var MANAGER_LINK = null;
|
|
var GROUPCHATS_JOIN = null;
|
|
var GROUPCHATS_SUGGEST = null;
|
|
var ENCRYPTION = null;
|
|
var HTTPS_STORAGE = null;
|
|
var HTTPS_FORCE = null;
|
|
var COMPRESSION = null;
|
|
var MULTI_FILES = null;
|
|
var DEVELOPER = null;
|
|
var REGISTER_API = null;
|
|
|
|
// Jappix hosts configuration
|
|
var HOST_MAIN = null;
|
|
var HOST_MUC = null;
|
|
var HOST_PUBSUB = null;
|
|
var HOST_VJUD = null;
|
|
var HOST_ANONYMOUS = null;
|
|
var HOST_BOSH = null;
|
|
var HOST_BOSH_MAIN = null;
|
|
var HOST_BOSH_MINI = null;
|
|
var HOST_STATIC = null;
|
|
var HOST_UPLOAD = null;
|
|
|
|
// Anonymous mode
|
|
var ANONYMOUS_ROOM = null;
|
|
var ANONYMOUS_NICK = null;
|
|
|
|
// Node parameters
|
|
var JAPPIX_LOCATION = getJappixLocation();
|
|
var BOSH_SAME_ORIGIN = false;
|
|
|
|
// XMPP error stanzas
|
|
function STANZA_ERROR(code, type, cond) {
|
|
if (window == this)
|
|
return new STANZA_ERROR(code, type, cond);
|
|
|
|
this.code = code;
|
|
this.type = type;
|
|
this.cond = cond;
|
|
}
|
|
|
|
var ERR_BAD_REQUEST =
|
|
STANZA_ERROR('400', 'modify', 'bad-request');
|
|
var ERR_CONFLICT =
|
|
STANZA_ERROR('409', 'cancel', 'conflict');
|
|
var ERR_FEATURE_NOT_IMPLEMENTED =
|
|
STANZA_ERROR('501', 'cancel', 'feature-not-implemented');
|
|
var ERR_FORBIDDEN =
|
|
STANZA_ERROR('403', 'auth', 'forbidden');
|
|
var ERR_GONE =
|
|
STANZA_ERROR('302', 'modify', 'gone');
|
|
var ERR_INTERNAL_SERVER_ERROR =
|
|
STANZA_ERROR('500', 'wait', 'internal-server-error');
|
|
var ERR_ITEM_NOT_FOUND =
|
|
STANZA_ERROR('404', 'cancel', 'item-not-found');
|
|
var ERR_JID_MALFORMED =
|
|
STANZA_ERROR('400', 'modify', 'jid-malformed');
|
|
var ERR_NOT_ACCEPTABLE =
|
|
STANZA_ERROR('406', 'modify', 'not-acceptable');
|
|
var ERR_NOT_ALLOWED =
|
|
STANZA_ERROR('405', 'cancel', 'not-allowed');
|
|
var ERR_NOT_AUTHORIZED =
|
|
STANZA_ERROR('401', 'auth', 'not-authorized');
|
|
var ERR_PAYMENT_REQUIRED =
|
|
STANZA_ERROR('402', 'auth', 'payment-required');
|
|
var ERR_RECIPIENT_UNAVAILABLE =
|
|
STANZA_ERROR('404', 'wait', 'recipient-unavailable');
|
|
var ERR_REDIRECT =
|
|
STANZA_ERROR('302', 'modify', 'redirect');
|
|
var ERR_REGISTRATION_REQUIRED =
|
|
STANZA_ERROR('407', 'auth', 'registration-required');
|
|
var ERR_REMOTE_SERVER_NOT_FOUND =
|
|
STANZA_ERROR('404', 'cancel', 'remote-server-not-found');
|
|
var ERR_REMOTE_SERVER_TIMEOUT =
|
|
STANZA_ERROR('504', 'wait', 'remote-server-timeout');
|
|
var ERR_RESOURCE_CONSTRAINT =
|
|
STANZA_ERROR('500', 'wait', 'resource-constraint');
|
|
var ERR_SERVICE_UNAVAILABLE =
|
|
STANZA_ERROR('503', 'cancel', 'service-unavailable');
|
|
var ERR_SUBSCRIPTION_REQUIRED =
|
|
STANZA_ERROR('407', 'auth', 'subscription-required');
|
|
var ERR_UNEXPECTED_REQUEST =
|
|
STANZA_ERROR('400', 'wait', 'unexpected-request');
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the temporary/persistent data store functions
|
|
|
|
-------------------------------------------------
|
|
|
|
License: dual-licensed under AGPL and MPLv2
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 20/07/13
|
|
|
|
*/
|
|
|
|
// Common: storage adapter
|
|
function storageAdapter(storage_native, storage_emulated) {
|
|
var legacy = !storage_native;
|
|
|
|
this.key = function(key) {
|
|
if(legacy) {
|
|
if(key >= this.length)
|
|
return null;
|
|
|
|
var c = 0;
|
|
|
|
for(name in storage_emulated) {
|
|
if(c++ == key) return name;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return storage_native.key(key);
|
|
};
|
|
|
|
this.getItem = function(key) {
|
|
if(legacy) {
|
|
if(storage_emulated[key] !== undefined)
|
|
return storage_emulated[key];
|
|
|
|
return null;
|
|
} else {
|
|
return storage_native.getItem(key);
|
|
}
|
|
};
|
|
|
|
this.setItem = function(key, data) {
|
|
if(legacy) {
|
|
if(!(key in storage_emulated))
|
|
this.length++;
|
|
|
|
storage_emulated[key] = (data + '');
|
|
} else {
|
|
storage_native.setItem(key, data);
|
|
this.length = storage_native.length;
|
|
}
|
|
};
|
|
|
|
this.removeItem = function(key) {
|
|
if(legacy) {
|
|
if(key in storage_emulated) {
|
|
this.length--;
|
|
delete storage_emulated[key];
|
|
}
|
|
} else {
|
|
storage_native.removeItem(key);
|
|
this.length = storage_native.length;
|
|
}
|
|
};
|
|
|
|
this.clear = function() {
|
|
if(legacy) {
|
|
this.length = 0;
|
|
storage_emulated = {};
|
|
} else {
|
|
storage_native.clear();
|
|
this.length = storage_native.length;
|
|
}
|
|
};
|
|
|
|
this.length = legacy ? 0 : storage_native.length;
|
|
}
|
|
|
|
|
|
// Temporary: sessionStorage emulation
|
|
var DATASTORE_DB_EMULATED = {};
|
|
|
|
// Temporary: sessionStorage class alias for direct access
|
|
var storageDB = new storageAdapter(
|
|
(window.sessionStorage ? sessionStorage : null),
|
|
DATASTORE_DB_EMULATED
|
|
);
|
|
|
|
// Temporary: returns whether it is available or not
|
|
function hasDB() {
|
|
// Try to write something
|
|
try {
|
|
storageDB.setItem('hasdb_check', 'ok');
|
|
storageDB.removeItem('hasdb_check');
|
|
|
|
return true;
|
|
}
|
|
|
|
// Not available?
|
|
catch(e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Temporary: used to read a database entry
|
|
function getDB(type, id) {
|
|
try {
|
|
return storageDB.getItem(type + '_' + id);
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while getting a temporary database entry (' + type + ' -> ' + id + '): ' + e, 1);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Temporary: used to update a database entry
|
|
function setDB(type, id, value) {
|
|
try {
|
|
storageDB.setItem(type + '_' + id, value);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while writing a temporary database entry (' + type + ' -> ' + id + '): ' + e, 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Temporary: used to remove a database entry
|
|
function removeDB(type, id) {
|
|
try {
|
|
storageDB.removeItem(type + '_' + id);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while removing a temporary database entry (' + type + ' -> ' + id + '): ' + e, 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Temporary: used to check a database entry exists
|
|
function existDB(type, id) {
|
|
return getDB(type, id) != null;
|
|
}
|
|
|
|
// Temporary: used to clear all the database
|
|
function resetDB() {
|
|
try {
|
|
storageDB.clear();
|
|
|
|
logThis('Temporary database cleared.', 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while clearing temporary database: ' + e, 1);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// Persistent: localStorage emulation
|
|
var DATASTORE_PERSISTENT_EMULATED = {};
|
|
|
|
// Persistent: localStorage class alias for direct access
|
|
var storagePersistent = new storageAdapter(
|
|
(window.localStorage ? localStorage : null),
|
|
DATASTORE_PERSISTENT_EMULATED
|
|
);
|
|
|
|
// Persistent: returns whether it is available or not
|
|
function hasPersistent() {
|
|
// Try to write something
|
|
try {
|
|
storagePersistent.setItem('haspersistent_check', 'ok');
|
|
storagePersistent.removeItem('haspersistent_check');
|
|
|
|
return true;
|
|
}
|
|
|
|
// Not available?
|
|
catch(e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Persistent: used to read a database entry
|
|
function getPersistent(dbID, type, id) {
|
|
try {
|
|
return storagePersistent.getItem(dbID + '_' + type + '_' + id);
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while getting a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + '): ' + e, 1);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Persistent: used to update a database entry
|
|
function setPersistent(dbID, type, id, value) {
|
|
try {
|
|
storagePersistent.setItem(dbID + '_' + type + '_' + id, value);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Database might be full
|
|
catch(e) {
|
|
logThis('Retrying: could not write a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + '): ' + e, 2);
|
|
|
|
// Flush it!
|
|
flushPersistent();
|
|
|
|
// Set the item again
|
|
try {
|
|
storagePersistent.setItem(dbID + ' -> ' + type + '_' + id, value);
|
|
|
|
return true;
|
|
}
|
|
|
|
// New error!
|
|
catch(e) {
|
|
logThis('Aborted: error while writing a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + '): ' + e, 1);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Persistent: used to remove a database entry
|
|
function removePersistent(dbID, type, id) {
|
|
try {
|
|
storagePersistent.removeItem(dbID + '_' + type + '_' + id);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while removing a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + '): ' + e, 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Persistent: used to check a database entry exists
|
|
function existPersistent(dbID, type, id) {
|
|
return getPersistent(dbID, type, id) != null;
|
|
}
|
|
|
|
// Persistent: used to clear all the database
|
|
function resetPersistent() {
|
|
try {
|
|
storagePersistent.clear();
|
|
|
|
logThis('Persistent database cleared.', 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while clearing persistent database: ' + e, 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Persistent: used to flush the database
|
|
function flushPersistent() {
|
|
try {
|
|
// Get the stored session entry
|
|
var session = getPersistent('global', 'session', 1);
|
|
|
|
// Reset the persistent database
|
|
resetPersistent();
|
|
|
|
// Restaure the stored session entry
|
|
if(session)
|
|
setPersistent('global', 'session', 1, session);
|
|
|
|
logThis('Persistent database flushed.', 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
logThis('Error while flushing persistent database: ' + e, 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
/* BROWSER DETECT
|
|
* http://www.quirksmode.org/js/detect.html
|
|
* License: dual-licensed under MPLv2 and the original license
|
|
*/
|
|
|
|
var BrowserDetect = {
|
|
init: function () {
|
|
this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
|
|
this.version = this.searchVersion(navigator.userAgent)
|
|
|| this.searchVersion(navigator.appVersion)
|
|
|| "an unknown version";
|
|
this.OS = this.searchString(this.dataOS) || "an unknown OS";
|
|
},
|
|
|
|
searchString: function (data) {
|
|
for (var i=0;i<data.length;i++) {
|
|
var dataString = data[i].string;
|
|
var dataProp = data[i].prop;
|
|
this.versionSearchString = data[i].versionSearch || data[i].identity;
|
|
if (dataString) {
|
|
if (dataString.indexOf(data[i].subString) != -1)
|
|
return data[i].identity;
|
|
}
|
|
else if (dataProp)
|
|
return data[i].identity;
|
|
}
|
|
},
|
|
|
|
searchVersion: function (dataString) {
|
|
var index = dataString.indexOf(this.versionSearchString);
|
|
if (index == -1) return;
|
|
return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
|
|
},
|
|
|
|
dataBrowser: [
|
|
{
|
|
string: navigator.userAgent,
|
|
subString: "Chrome",
|
|
identity: "Chrome"
|
|
},
|
|
{ string: navigator.userAgent,
|
|
subString: "OmniWeb",
|
|
versionSearch: "OmniWeb/",
|
|
identity: "OmniWeb"
|
|
},
|
|
{
|
|
string: navigator.vendor,
|
|
subString: "Apple",
|
|
identity: "Safari",
|
|
versionSearch: "Version"
|
|
},
|
|
{
|
|
prop: window.opera,
|
|
identity: "Opera"
|
|
},
|
|
{
|
|
string: navigator.vendor,
|
|
subString: "iCab",
|
|
identity: "iCab"
|
|
},
|
|
{
|
|
string: navigator.vendor,
|
|
subString: "KDE",
|
|
identity: "Konqueror"
|
|
},
|
|
{
|
|
string: navigator.userAgent,
|
|
subString: "Firefox",
|
|
identity: "Firefox"
|
|
},
|
|
{
|
|
string: navigator.vendor,
|
|
subString: "Camino",
|
|
identity: "Camino"
|
|
},
|
|
{ // for newer Netscapes (6+)
|
|
string: navigator.userAgent,
|
|
subString: "Netscape",
|
|
identity: "Netscape"
|
|
},
|
|
{
|
|
string: navigator.userAgent,
|
|
subString: "MSIE",
|
|
identity: "Explorer",
|
|
versionSearch: "MSIE"
|
|
},
|
|
{
|
|
string: navigator.userAgent,
|
|
subString: "Gecko",
|
|
identity: "Mozilla",
|
|
versionSearch: "rv"
|
|
},
|
|
{ // for older Netscapes (4-)
|
|
string: navigator.userAgent,
|
|
subString: "Mozilla",
|
|
identity: "Netscape",
|
|
versionSearch: "Mozilla"
|
|
}
|
|
],
|
|
|
|
dataOS : [
|
|
{
|
|
string: navigator.platform,
|
|
subString: "Win",
|
|
identity: "Windows"
|
|
},
|
|
{
|
|
string: navigator.platform,
|
|
subString: "Mac",
|
|
identity: "Mac"
|
|
},
|
|
{
|
|
string: navigator.userAgent,
|
|
subString: "iPhone",
|
|
identity: "iPhone/iPod"
|
|
},
|
|
{
|
|
string: navigator.platform,
|
|
subString: "Linux",
|
|
identity: "Linux"
|
|
}
|
|
]
|
|
};
|
|
|
|
BrowserDetect.init();
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the homepage JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, LinkMauve
|
|
Last revision: 05/03/13
|
|
|
|
*/
|
|
|
|
// Allows the user to switch the difference home page elements
|
|
function switchHome(div) {
|
|
// Path to
|
|
var home = '#home .';
|
|
var right = home + 'right ';
|
|
var current = right + '.homediv.' + div;
|
|
|
|
// We switch the div
|
|
$(right + '.homediv, ' + right + '.top').hide();
|
|
$(right + '.' + div).show();
|
|
|
|
// We reset the homedivs
|
|
$(home + 'homediv:not(.default), ' + home + 'top:not(.default)').remove();
|
|
|
|
// Get the HTML code to display
|
|
var disable_form = '';
|
|
var lock_host = '';
|
|
var code = '';
|
|
|
|
// Apply the previous link
|
|
switch(div) {
|
|
case 'loginer':
|
|
case 'anonymouser':
|
|
case 'registerer':
|
|
if(!exists(right + '.top.sub')) {
|
|
// Append the HTML code for previous link
|
|
$(right + '.top.default').after('<h1 class="top sub loginer anonymouser registerer">« <a href="#" class="previous">' + _e("Previous") + '</a></h1>');
|
|
|
|
// Click event on previous link
|
|
$(home + 'top.sub a.previous').click(function() {
|
|
return switchHome('default');
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// Apply the form
|
|
switch(div) {
|
|
// Login tool
|
|
case 'loginer':
|
|
lock_host = disableInput(LOCK_HOST, 'on');
|
|
code = '<p>' + printf(_e("Login to your existing XMPP account. You can also use the %s to join a groupchat."), '<a href="#" class="to-anonymous">' + _e("anonymous mode") + '</a>') + '</p>' +
|
|
|
|
'<form action="#" method="post">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Required") + '</legend>' +
|
|
|
|
'<label for="lnick">' + _e("Address") + '</label>' +
|
|
'<input type="text" class="nick" id="lnick" pattern="[^@/]+" required="" /><span class="jid">@</span><input type="text" class="server" id="lserver" value="' + HOST_MAIN + '" ' + lock_host + ' pattern="[^@/]+" required="" />' +
|
|
'<label for="lpassword">' + _e("Password") + '</label>' +
|
|
'<input type="password" class="password" id="lpassword" required="" />' +
|
|
'<label for="lremember">' + _e("Remember me") + '</label>' +
|
|
'<input type="checkbox" class="remember" id="lremember" />' +
|
|
'</fieldset>' +
|
|
|
|
'<a href="#" class="advanced home-images">' + _e("Advanced") + '</a>' +
|
|
|
|
'<fieldset class="advanced">' +
|
|
'<legend>' + _e("Advanced") + '</legend>' +
|
|
|
|
'<label for="lresource">' + _e("Resource") + '</label>' +
|
|
'<input type="text" class="resource" id="lresource" value="' + JAPPIX_RESOURCE + '" />' +
|
|
'<label for="lpriority">' + _e("Priority") + '</label>' +
|
|
'<select class="priority" id="lpriority">' +
|
|
'<option value="1">' + _e("Low") + '</option>' +
|
|
'<option value="10" selected="">' + _e("Medium") + '</option>' +
|
|
'<option value="100">' + _e("High") + '</option>' +
|
|
'</select>' +
|
|
'</fieldset>' +
|
|
|
|
'<input type="submit" value="' + _e("Here we go!") + '" />' +
|
|
'</form>';
|
|
|
|
break;
|
|
|
|
// Anonymous login tool
|
|
case 'anonymouser':
|
|
disable_form = disableInput(ANONYMOUS, 'off');
|
|
code = '<p>' + printf(_e("Enter the groupchat you want to join and the nick you want to have. You can also go back to the %s."), '<a href="#" class="to-home">' + _e("login page") + '</a>') + '</p>';
|
|
|
|
if(LEGAL)
|
|
code += '<p>' + printf(_e("By using our service, you accept %s."), '<b><a href="' + encodeQuotes(LEGAL) + '" target="_blank">' + _e("our terms of use") + '</a></b>') + '</p>';
|
|
|
|
code += '<form action="#" method="post">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Required") + '</legend>' +
|
|
|
|
'<label>' + _e("Room") + '</label>' +
|
|
'<input type="text" class="room"' + disable_form + ' pattern="[^/]+" required="" />' +
|
|
|
|
'<label>' + _e("Nickname") + '</label>' +
|
|
'<input type="text" class="nick"' + disable_form + ' required="" />' +
|
|
'</fieldset>' +
|
|
|
|
'<input type="submit" value="' + _e("Here we go!") + '"' + disable_form + ' />' +
|
|
'</form>' +
|
|
|
|
'<div class="info report">' +
|
|
_e("Share this link with your friends:") + ' <span></span>' +
|
|
'</div>';
|
|
|
|
break;
|
|
|
|
// Register tool
|
|
case 'registerer':
|
|
disable_form = disableInput(REGISTRATION, 'off');
|
|
|
|
if(!disable_form)
|
|
lock_host = disableInput(LOCK_HOST, 'on');
|
|
|
|
code = '<p>' + _e("Register a new XMPP account to join your friends on your own social cloud. That's simple!") + '</p>';
|
|
|
|
if(LEGAL)
|
|
code += '<p>' + printf(_e("By using our service, you accept %s."), '<b><a href="' + encodeQuotes(LEGAL) + '" target="_blank">' + _e("our terms of use") + '</a></b>') + '</p>';
|
|
|
|
code += '<form action="#" method="post">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Required") + '</legend>' +
|
|
|
|
'<label for="rnick">' + _e("Address") + '</label>' +
|
|
'<input type="text" class="nick" id="rnick" ' + disable_form + ' pattern="[^@/]+" required="" placeholder="' + _e("Username") + '" /><span class="jid">@</span><input type="text" class="server" id="rserver" value="' + HOST_MAIN + '" ' + disable_form + lock_host + ' pattern="[^@/]+" required="" placeholder="' + _e("Server") + '" />' +
|
|
'<label for="rpassword">' + _e("Password") + '</label>' +
|
|
'<input type="password" class="password" id="rpassword" ' + disable_form + ' required="" placeholder="' + _e("Enter password") + '" /><input type="password" class="spassword" id="spassword" ' + disable_form + ' required="" placeholder="' + _e("Once again...") + '" />';
|
|
|
|
if(REGISTER_API == 'on')
|
|
code += '<div class="captcha_grp">' +
|
|
'<label for="captcha">' + _e("Code") + '</label><input type="text" class="captcha" id="captcha" ' + disable_form + ' maxlength="6" pattern="[a-zA-Z0-9]{6}" required="" placeholder="' + _e("Security code") + '" /><img class="captcha_img" src="./php/captcha.php?id=' + genID() + '" alt="" />' +
|
|
'</div>';
|
|
|
|
code += '</fieldset>' +
|
|
|
|
'<input type="submit" value="' + _e("Here we go!") + '" ' + disable_form + '/>' +
|
|
'</form>';
|
|
|
|
break;
|
|
}
|
|
|
|
// Form disabled?
|
|
if(disable_form)
|
|
code += '<div class="info fail">' +
|
|
_e("This tool has been disabled!") +
|
|
'</div>';
|
|
|
|
// Create this HTML code
|
|
if(code && !exists(current)) {
|
|
// Append it!
|
|
$(right + '.homediv.default').after('<div class="' + div + ' homediv">' + code + '</div>');
|
|
|
|
// Create the attached events
|
|
switch(div) {
|
|
// Login tool
|
|
case 'loginer':
|
|
$(current + ' a.to-anonymous').click(function() {
|
|
return switchHome('anonymouser');
|
|
});
|
|
|
|
$(current + ' a.advanced').click(showAdvanced);
|
|
|
|
$(current + ' form').submit(loginForm);
|
|
|
|
break;
|
|
|
|
// Anonymous login tool
|
|
case 'anonymouser':
|
|
$(current + ' a.to-home').click(function() {
|
|
return switchHome('loginer');
|
|
});
|
|
|
|
$(current + ' form').submit(doAnonymous);
|
|
|
|
// Keyup event on anonymous join's room input
|
|
$(current + ' input.room').keyup(function() {
|
|
var value = $(this).val();
|
|
var report = current + ' .report';
|
|
var span = report + ' span';
|
|
|
|
if(!value) {
|
|
$(report).hide();
|
|
$(span).text('');
|
|
}
|
|
|
|
else {
|
|
$(report).show();
|
|
$(span).text(JAPPIX_LOCATION + '?r=' + value);
|
|
}
|
|
});
|
|
|
|
break;
|
|
|
|
// Register tool
|
|
case 'registerer':
|
|
// Server input change
|
|
$('#home input.server').keyup(function(e) {
|
|
if(trim($(this).val()) == HOST_MAIN) {
|
|
$('#home .captcha_grp').show();
|
|
$('#home input.captcha').removeAttr('disabled');
|
|
} else {
|
|
$('#home .captcha_grp').hide();
|
|
$('#home input.captcha').attr('disabled', true);
|
|
}
|
|
});
|
|
|
|
// Register input placeholder
|
|
// FIXME: breaks IE compatibility
|
|
//$('#home input[placeholder]').placeholder();
|
|
|
|
// Register form submit
|
|
$(current + ' form').submit(registerForm);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We focus on the first input
|
|
$(document).oneTime(10, function() {
|
|
$(right + 'input:visible:first').focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Allows the user to display the advanced login options
|
|
function showAdvanced() {
|
|
// Hide the link
|
|
$('#home a.advanced').hide();
|
|
|
|
// Show the fieldset
|
|
$('#home fieldset.advanced').show();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Reads the login form values
|
|
function loginForm() {
|
|
// We get the values
|
|
var lPath = '#home .loginer ';
|
|
var lServer = $(lPath + '.server').val();
|
|
var lNick = nodeprep($(lPath + '.nick').val());
|
|
var lPass = $(lPath + '.password').val();
|
|
var lResource = $(lPath + '.resource').val();
|
|
var lPriority = $(lPath + '.priority').val();
|
|
var lRemember = $(lPath + '.remember').filter(':checked').size();
|
|
|
|
// Enough values?
|
|
if(lServer && lNick && lPass && lResource && lPriority) {
|
|
doLogin(lNick, lServer, lPass, lResource, lPriority, lRemember);
|
|
} else {
|
|
$(lPath + 'input[type="text"], ' + lPath + 'input[type="password"]').each(function() {
|
|
var select = $(this);
|
|
|
|
if(!select.val())
|
|
$(document).oneTime(10, function() {
|
|
select.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
select.removeClass('please-complete');
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Reads the register form values
|
|
function registerForm() {
|
|
var rPath = '#home .registerer ';
|
|
|
|
// Remove the success info
|
|
$(rPath + '.success').remove();
|
|
|
|
// Get the values
|
|
var username = nodeprep($(rPath + '.nick').val());
|
|
var domain = $(rPath + '.server').val();
|
|
var pass = $(rPath + '.password').val();
|
|
var spass = $(rPath + '.spassword').val();
|
|
var captcha = $(rPath + '.captcha').val();
|
|
|
|
// Enough values?
|
|
if(domain && username && pass && spass && (pass == spass) && !((REGISTER_API == 'on') && (domain == HOST_MAIN) && !captcha)) {
|
|
// We remove the not completed class to avoid problems
|
|
$('#home .registerer input').removeClass('please-complete');
|
|
|
|
// Fire the register event!
|
|
doRegister(username, domain, pass, captcha);
|
|
}
|
|
|
|
// Something is missing?
|
|
else {
|
|
$(rPath + 'input[type="text"], ' + rPath + 'input[type="password"]').each(function() {
|
|
var select = $(this);
|
|
|
|
if(!select.val() || (select.is('#spassword') && pass && (pass != spass)))
|
|
$(document).oneTime(10, function() {
|
|
select.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
select.removeClass('please-complete');
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchHome() {
|
|
// Define the vars
|
|
var home = '#home ';
|
|
var button = home + 'button';
|
|
var corp = home + '.corporation';
|
|
var aboutus = home + '.aboutus';
|
|
var locale = home + '.locale';
|
|
|
|
// Removes the <noscript /> elements to lighten the DOM
|
|
$('noscript').remove();
|
|
|
|
// Allows the user to switch the home page
|
|
$(button).click(function() {
|
|
// Login button
|
|
if($(this).is('.login'))
|
|
return switchHome('loginer');
|
|
|
|
// Register button
|
|
else
|
|
return switchHome('registerer');
|
|
});
|
|
|
|
// Allows the user to view the corporation & about infobox
|
|
$(corp + ', ' + aboutus).hover(function() {
|
|
$(this).addClass('hovered');
|
|
}, function() {
|
|
$(this).removeClass('hovered');
|
|
});
|
|
|
|
// Allows the user to switch the language
|
|
$(locale).hover(function() {
|
|
// Initialize the HTML code
|
|
var keepget = $(locale).attr('data-keepget');
|
|
var html = '<div class="list">';
|
|
|
|
// Generate each locale HTML code
|
|
for(i in LOCALES_AVAILABLE_ID)
|
|
html += '<a href="./?l=' + LOCALES_AVAILABLE_ID[i] + keepget + '">' + LOCALES_AVAILABLE_NAMES[i].htmlEnc() + '</a>';
|
|
|
|
html += '</div>';
|
|
|
|
// Append the HTML code
|
|
$(locale).append(html);
|
|
}, function() {
|
|
$(locale + ' .list').remove();
|
|
});
|
|
|
|
// Disables the browser HTTP-requests stopper
|
|
$(document).keydown(function(e) {
|
|
if((e.keyCode == 27) && !isDeveloper())
|
|
return false;
|
|
});
|
|
|
|
// Warns for an obsolete browser
|
|
if(isObsolete()) {
|
|
// Add the code
|
|
$(locale).after(
|
|
'<div class="obsolete">' +
|
|
'<p>' + _e("Your browser is out of date!") + '</p>' +
|
|
|
|
'<a class="firefox browsers-images" title="' + printf(_e("Last %s version is better!"), 'Mozilla Firefox') + '" href="http://www.mozilla.com/firefox/"></a>' +
|
|
'<a class="chrome browsers-images" title="' + printf(_e("Last %s version is better!"), 'Google Chrome') + '" href="http://www.google.com/chrome"></a>' +
|
|
'<a class="safari browsers-images" title="' + printf(_e("Last %s version is better!"), 'Safari') + '" href="http://www.apple.com/safari/"></a>' +
|
|
'<a class="opera browsers-images" title="' + printf(_e("Last %s version is better!"), 'Opera') + '" href="http://www.opera.com/"></a>' +
|
|
'<a class="ie browsers-images" title="' + printf(_e("Last %s version is better!"), 'Internet Explorer') + '" href="http://www.microsoft.com/hk/windows/internet-explorer/"></a>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Display it later
|
|
$(home + '.obsolete').oneTime('1s', function() {
|
|
$(this).slideDown();
|
|
});
|
|
|
|
logThis('Jappix does not support this browser!', 2);
|
|
}
|
|
|
|
logThis('Welcome to Jappix! Happy coding in developer mode!');
|
|
}
|
|
|
|
// Launch this plugin!
|
|
$(document).ready(launchHome);
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the talkpage JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 06/05/11
|
|
|
|
*/
|
|
|
|
// Creates the talkpage events
|
|
function eventsTalkPage() {
|
|
// Launch all associated plugins
|
|
launchMicroblog();
|
|
launchRoster();
|
|
launchPresence();
|
|
launchPEP();
|
|
launchNotifications();
|
|
launchMusic();
|
|
}
|
|
|
|
// Creates the talkpage code
|
|
function createTalkPage() {
|
|
// Talkpage exists?
|
|
if(exists('#talk'))
|
|
return false;
|
|
|
|
// Anonymous detector
|
|
var anonymous = isAnonymous();
|
|
|
|
// Generate the HTML code
|
|
var html =
|
|
'<div id="talk" class="removable">' +
|
|
'<div id="top-content">' +
|
|
'<div class="tools tools-logo talk-images"></div>' +
|
|
|
|
'<div class="tools tools-all">';
|
|
|
|
if(!anonymous) html +=
|
|
'<a href="#" onclick="return openInbox();" class="inbox-hidable">' + _e("Messages") + '</a>' +
|
|
'<a href="#" onclick="return openVCard();" class="vcard">' + _e("Profile") + '</a>' +
|
|
'<a href="#" onclick="return optionsOpen();" class="options-hidable">' + _e("Options") + '</a>' +
|
|
'<a href="#" onclick="return normalQuit();" class="quit">' + _e("Disconnect") + '</a>';
|
|
|
|
else html +=
|
|
'<a href="./">' + _e("Disconnect") + '</a>';
|
|
|
|
html +=
|
|
'</div>';
|
|
|
|
if(!anonymous && document.createElement('audio').canPlayType) html +=
|
|
'<div class="tools-all ibubble">' +
|
|
'<div class="tools music talk-images" onclick="return openMusic();"></div>' +
|
|
|
|
'<div class="music-content tools-content bubble hidable">' +
|
|
'<div class="tools-content-subarrow talk-images"></div>' +
|
|
|
|
'<div class="tools-content-subitem">' +
|
|
'<div class="player">' +
|
|
'<a href="#" class="stop talk-images" onclick="return actionMusic(\'stop\');"></a>' +
|
|
'</div>' +
|
|
|
|
'<div class="list">' +
|
|
'<p class="no-results">' + _e("No result!") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="search">' +
|
|
'<input type="text" />' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
if(!anonymous) html +=
|
|
'<div class="tools-all ibubble">' +
|
|
'<div class="tools notifications talk-images" onclick="return showBubble(\'.notifications-content\');"></div>' +
|
|
|
|
'<div class="notifications-content tools-content bubble hidable">' +
|
|
'<div class="tools-content-subarrow talk-images"></div>' +
|
|
|
|
'<div class="tools-content-subitem">' +
|
|
'<a class="empty" href="#" onclick="return clearNotifications();">' + _e("Empty") + '</a>' +
|
|
'<p class="nothing">' + _e("No notifications.") + '</p>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
html +=
|
|
'</div>' +
|
|
|
|
'<div id="main-content">' +
|
|
'<div id="left-content">';
|
|
if(!anonymous) html +=
|
|
'<div id="buddy-list">' +
|
|
'<div class="content"></div>' +
|
|
|
|
'<div class="filter">' +
|
|
'<input type="text" placeholder="' + _e("Filter") + '" />' +
|
|
'<a href="#">x</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="foot ibubble">' +
|
|
'<div class="buddy-list-add buddy-list-icon">' +
|
|
'<a href="#" class="add talk-images" title="' + _e("Add a friend") + '"></a>' +
|
|
'</div>' +
|
|
|
|
'<div class="buddy-list-join buddy-list-icon">' +
|
|
'<a href="#" class="join talk-images" title="' + _e("Join a chat") + '"></a>' +
|
|
'</div>' +
|
|
|
|
'<div class="buddy-list-groupchat buddy-list-icon">' +
|
|
'<a href="#" class="groupchat talk-images" title="' + _e("Your groupchats") + '"></a>' +
|
|
'</div>' +
|
|
|
|
'<div class="buddy-list-more buddy-list-icon">' +
|
|
'<a href="#" class="more talk-images" title="' + _e("More stuff") + '"></a>' +
|
|
'</div>' +
|
|
|
|
'<div style="clear: both;"></div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
html +=
|
|
'<div id="my-infos">' +
|
|
'<div class="content">' +
|
|
'<div class="element f-presence ibubble">' +
|
|
'<a href="#" class="icon picker disabled" data-value="available">' +
|
|
'<span class="talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<input id="presence-status" type="text" placeholder="' + _e("Status") + '" disabled="" />' +
|
|
'</div>';
|
|
|
|
if(!anonymous) html +=
|
|
'<div class="element f-mood pep-hidable ibubble">' +
|
|
'<a href="#" class="icon picker" data-value="happy">' +
|
|
'<span class="talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<input id="mood-text" type="text" placeholder="' + _e("Mood") + '" />' +
|
|
'</div>' +
|
|
|
|
'<div class="element f-activity pep-hidable ibubble">' +
|
|
'<a href="#" class="icon picker" data-value="exercising">' +
|
|
'<span class="talk-images activity-exercising"></span>' +
|
|
'</a>' +
|
|
|
|
'<input id="activity-text" type="text" placeholder="' + _e("Activity") + '" />' +
|
|
'</div>';
|
|
|
|
html +=
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div id="right-content">' +
|
|
'<div id="page-switch">' +
|
|
'<div class="chans">';
|
|
if(!anonymous) html +=
|
|
'<div class="channel switcher activechan" onclick="return switchChan(\'channel\');">' +
|
|
'<div class="icon talk-images"></div>' +
|
|
|
|
'<div class="name">' + _e("Channel") + '</div>' +
|
|
'</div>';
|
|
|
|
html +=
|
|
'</div>' +
|
|
|
|
'<div class="more ibubble">' +
|
|
'<div class="more-button talk-images" onclick="return loadChatSwitch();" title="' + _e("All tabs") + '"></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div id="page-engine">';
|
|
if(!anonymous) html +=
|
|
'<div id="channel" class="page-engine-chan" style="display: block;">' +
|
|
'<div class="top mixed ' + hex_md5(getXID()) + '">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="update">' +
|
|
'<p>' + _e("What\'s up with you?") + '</p>' +
|
|
|
|
'<div class="microblog-body">' +
|
|
'<input class="focusable" type="text" name="microblog_body" placeholder="' + _e("Type something you want to share with your friends...") + '" disabled="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="one-microblog-icon ibubble">' +
|
|
'<a href="#" onclick="return showBubble(\'#attach\');" title="' + _e("Attach a file") + '" class="postit attach talk-images"></a>' +
|
|
|
|
'<form id="attach" class="bubble hidable" action="./php/file-share.php" method="post" enctype="multipart/form-data">' +
|
|
'<div class="attach-subarrow talk-images"></div>' +
|
|
|
|
'<div class="attach-subitem">' +
|
|
'<p class="attach-p">' + _e("Attach a file") + '</p>' +
|
|
generateFileShare() +
|
|
'</div>' +
|
|
'</form>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="content mixed"></div>' +
|
|
|
|
'<div class="footer">' +
|
|
'<div class="sync talk-images">' + _e("You are synchronized with your network.") + '</div>' +
|
|
|
|
'<div class="unsync talk-images">' + _e("Cannot send anything: you can only receive notices!") + '</div>' +
|
|
|
|
'<div class="fetch wait-small">' + _e("Fetching the social channel...") + '</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
html +=
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// Create the HTML code
|
|
$('body').prepend(html);
|
|
|
|
// Adapt the buddy-list size
|
|
adaptRoster();
|
|
|
|
// Create JS events
|
|
eventsTalkPage();
|
|
|
|
// Start the auto idle functions
|
|
liveIdle();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Destroys the talkpage code
|
|
function destroyTalkPage() {
|
|
// Reset our database
|
|
resetDB();
|
|
|
|
// Reset some vars
|
|
STANZA_ID = 1;
|
|
BLIST_ALL = false;
|
|
FIRST_PRESENCE_SENT = false;
|
|
SEARCH_FILTERED = false;
|
|
AVATAR_PENDING = [];
|
|
JOIN_SUGGEST = [];
|
|
|
|
// Kill all timers, exept the board ones
|
|
$('*:not(#board .one-board)').stopTime();
|
|
|
|
// Kill the auto idle functions
|
|
dieIdle();
|
|
|
|
// We renitalise the html markup as its initiale look
|
|
$('.removable').remove();
|
|
pageTitle('home');
|
|
|
|
// Finally we show the homepage
|
|
$('#home').show();
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the popup JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 10/04/12
|
|
|
|
*/
|
|
|
|
// Creates a popup code
|
|
function createPopup(id, content) {
|
|
// Popup exists?
|
|
if(exists('#' + id))
|
|
return false;
|
|
|
|
// Popop on top of another one?
|
|
var top_of = exists('div.lock:has(div.popup)');
|
|
|
|
// Append the popup code
|
|
$('body').append(
|
|
'<div id="' + id + '" class="lock removable">' +
|
|
'<div class="popup">' +
|
|
content +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Avoids darker popup background (if on top of another popup)
|
|
if(top_of)
|
|
$('#' + id).css('background', 'transparent');
|
|
|
|
// Attach popup events
|
|
launchPopup(id);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Destroys a popup code
|
|
function destroyPopup(id) {
|
|
// Stop the popup timers
|
|
$('#' + id + ' *').stopTime();
|
|
|
|
// Remove the popup
|
|
$('#' + id).remove();
|
|
|
|
// Manage input focus
|
|
inputFocus();
|
|
}
|
|
|
|
// Attaches popup events
|
|
function launchPopup(id) {
|
|
// Click events
|
|
$('#' + id).click(function(evt) {
|
|
// Click on lock background?
|
|
if($(evt.target).is('.lock:not(.unavoidable)')) {
|
|
// Destroy the popup
|
|
destroyPopup(id);
|
|
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the audio JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 04/05/12
|
|
|
|
*/
|
|
|
|
// Plays the given sound ID
|
|
function soundPlay(num) {
|
|
try {
|
|
// Not supported!
|
|
if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9))
|
|
return false;
|
|
|
|
// If the sounds are enabled
|
|
if(getDB('options', 'sounds') == '1') {
|
|
// If the audio elements aren't yet in the DOM
|
|
if(!exists('#audio')) {
|
|
$('body').append(
|
|
'<div id="audio">' +
|
|
'<audio id="new-chat" preload="auto">' +
|
|
'<source src="./snd/new-chat.mp3" />' +
|
|
'<source src="./snd/new-chat.oga" />' +
|
|
'</audio>' +
|
|
|
|
'<audio id="receive-message" preload="auto">' +
|
|
'<source src="./snd/receive-message.mp3" />' +
|
|
'<source src="./snd/receive-message.oga" />' +
|
|
'</audio>' +
|
|
|
|
'<audio id="notification" preload="auto">' +
|
|
'<source src="./snd/notification.mp3" />' +
|
|
'<source src="./snd/notification.oga" />' +
|
|
'</audio>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
|
|
// We play the target sound
|
|
var playThis = document.getElementById('audio').getElementsByTagName('audio')[num];
|
|
|
|
// Fixes Chrome audio bug when Get API serves expired files (for development work purposes)
|
|
if(window.chrome && isDeveloper())
|
|
playThis.load();
|
|
|
|
playThis.play();
|
|
}
|
|
}
|
|
|
|
catch(e) {}
|
|
|
|
finally {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the notification board JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou, Maranda
|
|
Last revision: 20/02/13
|
|
|
|
*/
|
|
|
|
// Creates a board panel
|
|
function createBoard(type, id) {
|
|
// Text var
|
|
var text = '';
|
|
|
|
// Info
|
|
if(type == 'info') {
|
|
switch(id) {
|
|
// Password change
|
|
case 1:
|
|
text = _e("Your password has been changed, now you can connect to your account with your new login data.");
|
|
|
|
break;
|
|
|
|
// Account deletion
|
|
case 2:
|
|
text = _e("Your XMPP account has been removed, bye!");
|
|
|
|
break;
|
|
|
|
// Account logout
|
|
case 3:
|
|
text = _e("You have been logged out of your XMPP account, have a nice day!");
|
|
|
|
break;
|
|
|
|
// Groupchat join
|
|
case 4:
|
|
text = _e("The room you tried to join doesn't seem to exist.");
|
|
|
|
break;
|
|
|
|
// Groupchat removal
|
|
case 5:
|
|
text = _e("The groupchat has been removed.");
|
|
|
|
break;
|
|
|
|
// Non-existant groupchat user
|
|
case 6:
|
|
text = _e("The user that you want to reach is not present in the room.");
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Error
|
|
else {
|
|
switch(id) {
|
|
// Custom error
|
|
case 1:
|
|
text = '<b>' + _e("Error") + '</b> » <span></span>';
|
|
|
|
break;
|
|
|
|
// Network error
|
|
case 2:
|
|
text = _e("Jappix has been interrupted by a network issue, a bug or bad login (check that you entered the right credentials), sorry for the inconvenience.");
|
|
|
|
break;
|
|
|
|
// List retrieving error
|
|
case 3:
|
|
text = _e("The element list on this server could not be obtained!");
|
|
|
|
break;
|
|
|
|
// Attaching error
|
|
case 4:
|
|
text = printf(_e("An error occured while uploading your file: maybe it is too big (%s maximum) or forbidden!"), JAPPIX_MAX_UPLOAD);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No text?
|
|
if(!text)
|
|
return false;
|
|
|
|
// Append the content
|
|
$('#board').append('<div class="one-board ' + type + '" data-id="' + id + '">' + text + '</div>');
|
|
|
|
// Events (click and auto-hide)
|
|
$('#board .one-board.' + type + '[data-id="' + id + '"]')
|
|
|
|
.click(function() {
|
|
closeThisBoard(this);
|
|
})
|
|
|
|
.oneTime('5s', function() {
|
|
closeThisBoard(this);
|
|
})
|
|
|
|
.slideDown();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Destroys the existing board notifications
|
|
function destroyBoard() {
|
|
$('#board').empty();
|
|
}
|
|
|
|
// Executes a given action on the notification board
|
|
function actionBoard(id, type) {
|
|
// In a first, we destroy other boards
|
|
destroyBoard();
|
|
|
|
// Then we display the board
|
|
createBoard(type, id);
|
|
}
|
|
|
|
// Opens a given error ID
|
|
function openThisError(id) {
|
|
actionBoard(id, 'error');
|
|
}
|
|
|
|
// Opens a given info ID
|
|
function openThisInfo(id) {
|
|
actionBoard(id, 'info');
|
|
}
|
|
|
|
// Closes a given board
|
|
function closeThisBoard(board) {
|
|
$(board).slideUp('normal', function() {
|
|
$(this).remove();
|
|
});
|
|
}
|
|
|
|
// Creates a quick board (HTML5 notification)
|
|
function quickBoard(xid, type, content, title, icon) {
|
|
// Cannot process?
|
|
if(isFocused() || !content || !window.webkitNotifications)
|
|
return;
|
|
|
|
// Default icon?
|
|
if(!icon) {
|
|
icon = './img/others/default-avatar.png';
|
|
|
|
// Avatar icon?
|
|
if(xid) {
|
|
var avatar_xml = XMLFromString(getPersistent('global', 'avatar', xid));
|
|
var avatar_type = $(avatar_xml).find('type').text() || 'image/png';
|
|
var avatar_binval = $(avatar_xml).find('binval').text();
|
|
|
|
if(avatar_binval && avatar_type)
|
|
icon = 'data:' + avatar_type + ';base64,' + avatar_binval;
|
|
}
|
|
}
|
|
|
|
// Default title?
|
|
if(!title)
|
|
title = _e("New event!");
|
|
|
|
// Check for notification permission
|
|
if(window.webkitNotifications.checkPermission() == 0) {
|
|
// Create notification
|
|
var notification = window.webkitNotifications.createNotification(icon, title, content);
|
|
|
|
// Auto-hide after a while
|
|
notification.ondisplay = function(event) {
|
|
setTimeout(function() {
|
|
event.currentTarget.cancel();
|
|
}, 10000);
|
|
};
|
|
|
|
// Click event
|
|
notification.onclick = function() {
|
|
// Click action?
|
|
switch(type) {
|
|
case 'chat':
|
|
switchChan(hex_md5(xid));
|
|
break;
|
|
|
|
case 'groupchat':
|
|
switchChan(hex_md5(bareXID(xid)));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Focus on msg-me
|
|
window.focus();
|
|
|
|
// Remove notification
|
|
this.cancel();
|
|
};
|
|
|
|
// Show notification
|
|
notification.show();
|
|
|
|
return notification;
|
|
|
|
}
|
|
}
|
|
|
|
// Asks for permission to show quick boards (HTML5 notification)
|
|
function quickBoardPermission() {
|
|
if(!window.webkitNotifications || (window.webkitNotifications.checkPermission() == 0))
|
|
return;
|
|
|
|
// Ask for permission
|
|
window.webkitNotifications.requestPermission();
|
|
}
|
|
|
|
// Fires quickBoardPermission() on document click
|
|
$(document).click(function() {
|
|
// Ask for permission to use quick boards
|
|
if((typeof con != 'undefined') && con.connected())
|
|
quickBoardPermission();
|
|
});
|
|
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the bubble JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 11/12/10
|
|
|
|
*/
|
|
|
|
// Closes all the opened bubbles
|
|
function closeBubbles() {
|
|
// Destroy all the elements
|
|
$('.bubble.hidable:visible').hide();
|
|
$('.bubble.removable').remove();
|
|
$('body').off('click');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Click function when a bubble is opened
|
|
function showBubble(selector) {
|
|
// Hidable bubbles special things
|
|
if($(selector).is('.hidable')) {
|
|
// This bubble is yet displayed? So abort!
|
|
if($(selector).is(':visible'))
|
|
return closeBubbles();
|
|
|
|
// Close all the bubbles
|
|
closeBubbles();
|
|
|
|
// Show the requested bubble
|
|
$(selector).show();
|
|
}
|
|
|
|
// Removable bubbles special things
|
|
else {
|
|
// This bubble is yet added? So abort!
|
|
if(exists(selector))
|
|
return closeBubbles();
|
|
|
|
// Close all the bubbles
|
|
closeBubbles();
|
|
}
|
|
|
|
// Creates a new click event to close the bubble
|
|
$('body').on('click', function(evt) {
|
|
var target = evt.target;
|
|
|
|
// If this is a click away from a bubble
|
|
if(!$(target).parents('.ibubble').size())
|
|
closeBubbles();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the chat JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Eric, Maranda
|
|
Last revision: 20/02/13
|
|
|
|
*/
|
|
|
|
// Correctly opens a new chat
|
|
function checkChatCreate(xid, type, nickname, password, title) {
|
|
// No XID?
|
|
if(!xid)
|
|
return false;
|
|
|
|
// We generate some stuffs
|
|
var hash = hex_md5(xid);
|
|
var name;
|
|
|
|
// Gets the name of the user/title of the room
|
|
if(title)
|
|
name = title;
|
|
|
|
else {
|
|
// Private groupchat chat
|
|
if(type == 'private')
|
|
name = thisResource(xid);
|
|
|
|
// XMPP-ID
|
|
else if(xid.indexOf('@') != -1)
|
|
name = getBuddyName(xid);
|
|
|
|
// Gateway
|
|
else
|
|
name = xid;
|
|
}
|
|
|
|
// If the target div does not exist
|
|
if(!exists('#' + hash)) {
|
|
// We check the type of the chat to open
|
|
if((type == 'chat') || (type == 'private'))
|
|
chatCreate(hash, xid, name, type);
|
|
|
|
else if(type == 'groupchat') {
|
|
// Try to read the room stored configuration
|
|
if(!isAnonymous() && (!nickname || !password || !title)) {
|
|
// Catch the room data
|
|
var fData = $(XMLFromString(getDB('favorites', xid)));
|
|
var fNick = fData.find('nick').text();
|
|
var fPwd = fData.find('password').text();
|
|
var fName = fData.find('name').text();
|
|
|
|
// Apply the room data
|
|
if(!nickname && fNick)
|
|
nickname = fNick;
|
|
if(!password && fPwd)
|
|
password = fPwd;
|
|
if(!title && fName)
|
|
name = fName;
|
|
}
|
|
|
|
groupchatCreate(hash, xid, name, nickname, password);
|
|
}
|
|
}
|
|
|
|
// Switch to the newly-created chat
|
|
switchChan(hash);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Generates the chat DOM elements
|
|
function generateChat(type, id, xid, nick) {
|
|
// Generate some stuffs
|
|
var path = '#' + id + ' .';
|
|
var escaped_xid = escape(xid);
|
|
|
|
// Special code
|
|
var specialAttributes, specialAvatar, specialName, specialCode, specialLink, specialDisabled, specialStyle, specialMAM;
|
|
|
|
// Groupchat special code
|
|
if(type == 'groupchat') {
|
|
specialAttributes = ' data-type="groupchat"';
|
|
specialAvatar = '';
|
|
specialName = '<p class="bc-infos"><b>' + _e("Subject") + '</b> <span class="muc-topic">' + _e("no subject defined for this room.") + '</span></p>';
|
|
specialCode = '<div class="content groupchat-content" id="chat-content-' + id + '"></div><div class="list"><div class="moderator role"><p class="title">' + _e("Moderators") + '</p></div><div class="participant role"><p class="title">' + _e("Participants") + '</p></div><div class="visitor role"><p class="title">' + _e("Visitors") + '</p></div><div class="none role"><p class="title">' + _e("Others") + '</p></div></div>';
|
|
specialLink = '<a href="#" class="tools-mucadmin tools-tooltip talk-images chat-tools-content" title="' + _e("Administration panel for this room") + '"></a>';
|
|
specialStyle = '';
|
|
|
|
// Is this a gateway?
|
|
if(xid.match(/%/))
|
|
specialDisabled = '';
|
|
else
|
|
specialDisabled = ' disabled=""';
|
|
}
|
|
|
|
// Chat (or other things?!) special code
|
|
else {
|
|
specialMAM = '<div class="wait-mam wait-small"></div>';
|
|
specialAttributes = ' data-type="chat"';
|
|
specialAvatar = '<div class="avatar-container"><img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" /></div>';
|
|
specialName = '<div class="bc-pep"></div><p class="bc-infos"><span class="unavailable show talk-images"></span></p>';
|
|
specialCode = '<div class="content" id="chat-content-' + id + '">' + specialMAM + '</div>';
|
|
specialLink = '<a href="#" class="tools-infos tools-tooltip talk-images chat-tools-content" title="' + _e("Show user profile") + '"></a>';
|
|
specialStyle = ' style="display: none;"';
|
|
specialDisabled = '';
|
|
}
|
|
|
|
// Not a groupchat private chat, we can use the buddy add icon
|
|
if((type == 'chat') || (type == 'groupchat')) {
|
|
var addTitle;
|
|
|
|
if(type == 'chat')
|
|
addTitle = _e("Add this contact to your friends");
|
|
else
|
|
addTitle = _e("Add this groupchat to your favorites");
|
|
|
|
specialLink += '<a href="#" class="tools-add tools-tooltip talk-images chat-tools-content" title="' + addTitle + '"></a>';
|
|
}
|
|
|
|
// IE DOM parsing bug fix
|
|
var specialStylePicker = '<div class="chat-tools-content chat-tools-style"' + specialStyle + '>' +
|
|
'<a href="#" class="tools-style tools-tooltip talk-images"></a>' +
|
|
'</div>';
|
|
|
|
if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9))
|
|
specialStylePicker = '';
|
|
|
|
// Append the chat HTML code
|
|
$('#page-engine').append(
|
|
'<div id="' + id + '" class="page-engine-chan chat one-counter"' + specialAttributes + ' data-xid="' + escaped_xid + '">' +
|
|
'<div class="top ' + id + '">' +
|
|
specialAvatar +
|
|
|
|
'<div class="name">' +
|
|
'<p class="bc-name bc-name-nick">' + nick.htmlEnc() + '</p>' +
|
|
specialName +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
specialCode +
|
|
|
|
'<div class="text">' +
|
|
'<div class="footer">' +
|
|
'<div class="chat-tools-content chat-tools-smileys">' +
|
|
'<a href="#" class="tools-smileys tools-tooltip talk-images"></a>' +
|
|
'</div>' +
|
|
|
|
specialStylePicker +
|
|
|
|
'<div class="chat-tools-content chat-tools-file">' +
|
|
'<a href="#" class="tools-file tools-tooltip talk-images"></a>' +
|
|
'</div>' +
|
|
|
|
'<div class="chat-tools-content chat-tools-save">' +
|
|
'<a href="#" class="tools-save tools-tooltip talk-images"></a>' +
|
|
'</div>' +
|
|
|
|
'<a href="#" class="tools-clear tools-tooltip talk-images chat-tools-content" title="' + _e("Clean current chat") + '"></a>' +
|
|
|
|
specialLink +
|
|
'</div>' +
|
|
|
|
'<div class="compose">' +
|
|
'<textarea class="message-area focusable" ' + specialDisabled + ' data-to="' + escaped_xid + '" /></textarea>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Click event: chat cleaner
|
|
$(path + 'tools-clear').click(function() {
|
|
cleanChat(id);
|
|
});
|
|
|
|
// Click event: user-infos
|
|
$(path + 'tools-infos').click(function() {
|
|
openUserInfos(xid);
|
|
});
|
|
}
|
|
|
|
// Generates the chat switch elements
|
|
function generateSwitch(type, id, xid, nick) {
|
|
// Path to the element
|
|
var chat_switch = '#page-switch .';
|
|
|
|
// Special code
|
|
var specialClass = ' unavailable';
|
|
var show_close = true;
|
|
|
|
// Groupchat
|
|
if(type == 'groupchat') {
|
|
specialClass = ' groupchat-default';
|
|
|
|
if(isAnonymous() && (xid == generateXID(ANONYMOUS_ROOM, 'groupchat')))
|
|
show_close = false;
|
|
}
|
|
|
|
// Generate the HTML code
|
|
var html = '<div class="' + id + ' switcher chan" onclick="return switchChan(\'' + encodeOnclick(id) + '\')">' +
|
|
'<div class="icon talk-images' + specialClass + '"></div>' +
|
|
|
|
'<div class="name">' + nick.htmlEnc() + '</div>';
|
|
|
|
// Show the close button if not MUC and not anonymous
|
|
if(show_close)
|
|
html += '<div class="exit" title="' + _e("Close this tab") + '" onclick="return quitThisChat(\'' + encodeOnclick(xid) + '\', \'' + encodeOnclick(id) + '\', \'' + encodeOnclick(type) + '\');">x</div>';
|
|
|
|
// Close the HTML
|
|
html += '</div>';
|
|
|
|
// Append the HTML code
|
|
$(chat_switch + 'chans, ' + chat_switch + 'more-content').append(html);
|
|
}
|
|
|
|
// Cleans given the chat lines
|
|
function cleanChat(chat) {
|
|
// Remove the messages
|
|
$('#page-engine #' + chat + ' .content .one-group').remove();
|
|
|
|
// Clear the history database
|
|
removePersistent(getXID(), 'history', chat);
|
|
|
|
// Focus again
|
|
$(document).oneTime(10, function() {
|
|
$('#page-engine #' + chat + ' .text .message-area').focus();
|
|
});
|
|
}
|
|
|
|
// Creates a new chat
|
|
function chatCreate(hash, xid, nick, type) {
|
|
logThis('New chat: ' + xid, 3);
|
|
|
|
// Create the chat content
|
|
generateChat(type, hash, xid, nick);
|
|
|
|
// Create the chat switcher
|
|
generateSwitch(type, hash, xid, nick);
|
|
|
|
// If the user is not in our buddy-list
|
|
if(type == 'chat') {
|
|
// MAM? Get archives from there!
|
|
if(enabledMAM()) {
|
|
getArchivesMAM({
|
|
'with': xid
|
|
}, {
|
|
'max': MAM_REQ_MAX,
|
|
'before': ''
|
|
});
|
|
} else {
|
|
// Restore the chat history
|
|
var chat_history = getPersistent(getXID(), 'history', hash);
|
|
|
|
if(chat_history) {
|
|
// Generate hashs
|
|
var my_hash = hex_md5(getXID());
|
|
var friend_hash = hex_md5(xid);
|
|
|
|
// Add chat history HTML
|
|
$('#' + hash + ' .content').append(chat_history);
|
|
|
|
// Filter old groups & messages
|
|
$('#' + hash + ' .one-group[data-type="user-message"]').addClass('from-history').attr('data-type', 'old-message');
|
|
$('#' + hash + ' .user-message').removeClass('user-message').addClass('old-message');
|
|
|
|
// Regenerate user names
|
|
$('#' + hash + ' .one-group.' + my_hash + ' b.name').text(getBuddyName(getXID()));
|
|
$('#' + hash + ' .one-group.' + friend_hash + ' b.name').text(getBuddyName(xid));
|
|
|
|
// Regenerate group dates
|
|
$('#' + hash + ' .one-group').each(function() {
|
|
var current_stamp = parseInt($(this).attr('data-stamp'));
|
|
$(this).find('span.date').text(relativeDate(current_stamp));
|
|
});
|
|
|
|
// Regenerate avatars
|
|
if(exists('#' + hash + ' .one-group.' + my_hash + ' .avatar-container'))
|
|
getAvatar(getXID(), 'cache', 'true', 'forget');
|
|
if(exists('#' + hash + ' .one-group.' + friend_hash + ' .avatar-container'))
|
|
getAvatar(xid, 'cache', 'true', 'forget');
|
|
}
|
|
}
|
|
|
|
// Add button
|
|
if(!exists('#buddy-list .buddy[data-xid="' + escape(xid) + '"]'))
|
|
$('#' + hash + ' .tools-add').click(function() {
|
|
// Hide the icon (to tell the user all is okay)
|
|
$(this).hide();
|
|
|
|
// Send the subscribe request
|
|
addThisContact(xid, nick);
|
|
}).show();
|
|
}
|
|
|
|
// We catch the user's informations (like this avatar, vcard, and so on...)
|
|
getUserInfos(hash, xid, nick, type);
|
|
|
|
// The icons-hover functions
|
|
tooltipIcons(xid, hash);
|
|
|
|
// The event handlers
|
|
var inputDetect = $('#page-engine #' + hash + ' .message-area');
|
|
|
|
inputDetect.focus(function() {
|
|
// Clean notifications for this chat
|
|
chanCleanNotify(hash);
|
|
|
|
// Store focus on this chat!
|
|
CHAT_FOCUS_HASH = hash;
|
|
});
|
|
|
|
inputDetect.blur(function() {
|
|
// Reset storage about focus on this chat!
|
|
if(CHAT_FOCUS_HASH == hash)
|
|
CHAT_FOCUS_HASH = null;
|
|
});
|
|
|
|
inputDetect.keypress(function(e) {
|
|
// Enter key
|
|
if(e.keyCode == 13) {
|
|
// Add a new line
|
|
if(e.shiftKey)
|
|
inputDetect.val(inputDetect.val() + '\n');
|
|
|
|
// Send the message
|
|
else {
|
|
// Send the message
|
|
sendMessage(hash, 'chat');
|
|
|
|
// Reset the composing database entry
|
|
setDB('chatstate', xid, 'off');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Scroll in chat content
|
|
$('#page-engine #' + hash + ' .content').scroll(function() {
|
|
if(enabledMAM() && !(xid in MAM_MAP_PENDING)) {
|
|
var has_state = xid in MAM_MAP_STATES;
|
|
var rsm_count = has_state ? MAM_MAP_STATES[xid]['rsm']['count'] : 1;
|
|
var rsm_before = has_state ? MAM_MAP_STATES[xid]['rsm']['first'] : '';
|
|
|
|
// Request more archives?
|
|
if(rsm_count > 0 && $(this).scrollTop() < 200) {
|
|
var wait_mam = $('#' + hash).find('.wait-mam');
|
|
wait_mam.show();
|
|
|
|
getArchivesMAM({
|
|
'with': xid
|
|
}, {
|
|
'max': MAM_REQ_MAX,
|
|
'before': rsm_before
|
|
}, function() {
|
|
wait_mam.hide();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Chatstate events
|
|
eventsChatState(inputDetect, xid, hash, 'chat');
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the groupchat JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda, Eric
|
|
Last revision: 13/02/12
|
|
|
|
*/
|
|
|
|
// Displays the MUC admin elements
|
|
function displayMucAdmin(affiliation, id, xid, statuscode) {
|
|
// We must be in the "login" mode
|
|
if(isAnonymous())
|
|
return;
|
|
|
|
// We check if the user is a room owner or administrator to give him privileges
|
|
if(affiliation == 'owner' || affiliation == 'admin')
|
|
$('#' + id + ' .tools-mucadmin').show();
|
|
|
|
// We check if the room hasn't been yet created
|
|
if(statuscode == 201)
|
|
openThisInfo(4);
|
|
|
|
// We add the click event
|
|
$('#' + id + ' .tools-mucadmin').click(function() {
|
|
openMucAdmin(xid, affiliation);
|
|
});
|
|
}
|
|
|
|
// Initializes a connection with a MUC groupchat
|
|
function getMUC(room, nickname, password) {
|
|
// Room hash
|
|
var hash = hex_md5(room);
|
|
|
|
// Reset the elements
|
|
$('#' + hash + ' .muc-ask').remove();
|
|
$('#' + hash + ' .compose').show();
|
|
|
|
// No nickname?
|
|
if(!nickname) {
|
|
// Get some values
|
|
if(!isAnonymous())
|
|
nickname = getNick();
|
|
else
|
|
nickname = ANONYMOUS_NICK;
|
|
|
|
// If the nickname could not be retrieved, ask it
|
|
if(!nickname)
|
|
generateMUCAsk('nickname', room, hash, nickname, password);
|
|
}
|
|
|
|
// Got our nickname?
|
|
if(nickname) {
|
|
// Get our general presence
|
|
var show = getDB('presence-show', 1);
|
|
var status = getDB('options', 'presence-status');
|
|
|
|
// Set my nick
|
|
$('#' + hash).attr('data-nick', escape(nickname));
|
|
|
|
// Send the appropriate presence
|
|
sendPresence(room + '/' + nickname, '', show, status, '', true, password, handleMUC);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the MUC main elements
|
|
function handleMUC(presence) {
|
|
// We get the xml content
|
|
var xml = presence.getNode();
|
|
var from = fullXID(getStanzaFrom(presence));
|
|
var room = bareXID(from);
|
|
var nickname = thisResource(from);
|
|
var hash = hex_md5(room);
|
|
|
|
// No ID: must fix M-Link bug
|
|
if(presence.getID() == null)
|
|
presence.setID(1);
|
|
|
|
logThis('First MUC presence: ' + from, 3);
|
|
|
|
// Catch the errors
|
|
if(!handleError(xml)) {
|
|
// Define some stuffs
|
|
var muc_user = $(xml).find('x[xmlns="' + NS_MUC_USER + '"]');
|
|
var affiliation = muc_user.find('item').attr('affiliation');
|
|
var statuscode = parseInt(muc_user.find('status').attr('code'));
|
|
|
|
// Handle my presence
|
|
handlePresence(presence);
|
|
|
|
// Check if I am a room owner
|
|
displayMucAdmin(affiliation, hash, room, statuscode);
|
|
|
|
// Tell the MUC we can notify the incoming presences
|
|
$(document).oneTime('15s', function() {
|
|
$('#' + hash).attr('data-initial', 'true');
|
|
});
|
|
|
|
// Enable the chatting input
|
|
$(document).oneTime(10, function() {
|
|
$('#' + hash + ' .message-area').removeAttr('disabled').focus();
|
|
});
|
|
}
|
|
|
|
// A password is required
|
|
else if($(xml).find('error[type="auth"] not-authorized').size())
|
|
generateMUCAsk('password', room, hash, nickname);
|
|
|
|
// There's a nickname conflict
|
|
else if($(xml).find('error[type="cancel"] conflict').size())
|
|
generateMUCAsk('nickname', room, hash);
|
|
}
|
|
|
|
// Generates a correct MUC asker
|
|
function generateMUCAsk(type, room, hash, nickname, password) {
|
|
// Generate the path to the elements
|
|
var path_to = '#' + hash + ' .muc-ask';
|
|
|
|
// Define the label text
|
|
var label_text;
|
|
|
|
switch(type) {
|
|
case 'nickname':
|
|
label_text = _e("Nickname");
|
|
break;
|
|
|
|
case 'password':
|
|
label_text = _e("Password");
|
|
break;
|
|
}
|
|
|
|
// Create the HTML markup
|
|
$('#' + hash + ' .compose').hide();
|
|
|
|
$('#' + hash).append(
|
|
'<div class="muc-ask text">' +
|
|
'<label>' + label_text + '</label>' +
|
|
'<input class="focusable" type="text" />' +
|
|
'</div>'
|
|
);
|
|
|
|
// When a key is pressed in the input
|
|
$(path_to + ' input').keyup(function(e) {
|
|
var value_input = $(this).val();
|
|
|
|
// Enter key pressed
|
|
if(e.keyCode == 13) {
|
|
// trim() fixes #304
|
|
if(type == 'nickname' && trim(value_input)) {
|
|
nickname = trim(value_input);
|
|
return getMUC(room, nickname, password);
|
|
}
|
|
|
|
if(type == 'password' && value_input) {
|
|
password = value_input;
|
|
return getMUC(room, nickname, password);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Focus on the input
|
|
$(document).oneTime(10, function() {
|
|
$(path_to + ' input').focus();
|
|
});
|
|
}
|
|
|
|
// Creates a new groupchat
|
|
function groupchatCreate(hash, room, chan, nickname, password) {
|
|
/* REF: http://xmpp.org/extensions/xep-0045.html */
|
|
|
|
logThis('New groupchat: ' + room, 3);
|
|
|
|
// Create the chat content
|
|
generateChat('groupchat', hash, room, chan);
|
|
|
|
// Create the chat switcher
|
|
generateSwitch('groupchat', hash, room, chan);
|
|
|
|
// The icons-hover functions
|
|
tooltipIcons(room, hash);
|
|
|
|
// Click event on the add tool
|
|
$('#' + hash + ' .tools-add').click(function() {
|
|
// Hide the icon (to tell the user all is okay)
|
|
$(this).hide();
|
|
|
|
// Add the groupchat to the user favorites
|
|
addThisFavorite(room, chan);
|
|
});
|
|
|
|
// Must show the add button?
|
|
if(!existDB('favorites', room))
|
|
$('#' + hash + ' .tools-add').show();
|
|
|
|
// The event handlers
|
|
var inputDetect = $('#' + hash + ' .message-area');
|
|
|
|
// Focus event
|
|
inputDetect.focus(function() {
|
|
// Clean notifications for this chat
|
|
chanCleanNotify(hash);
|
|
|
|
// Store focus on this chat!
|
|
CHAT_FOCUS_HASH = hash;
|
|
})
|
|
|
|
// Blur event
|
|
inputDetect.blur(function() {
|
|
// Reset storage about focus on this chat!
|
|
if(CHAT_FOCUS_HASH == hash)
|
|
CHAT_FOCUS_HASH = null;
|
|
|
|
// Reset autocompletion
|
|
resetAutocompletion(hash);
|
|
})
|
|
|
|
// Lock to the input
|
|
inputDetect.keydown(function(e) {
|
|
// Enter key
|
|
if(e.keyCode == 13) {
|
|
// Add a new line
|
|
if(e.shiftKey)
|
|
inputDetect.val(inputDetect.val() + '\n');
|
|
|
|
// Send the message
|
|
else {
|
|
sendMessage(hash, 'groupchat');
|
|
|
|
// Reset the composing database entry
|
|
setDB('chatstate', room, 'off');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Tabulation key
|
|
else if(e.keyCode == 9) {
|
|
createAutocompletion(hash);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Reset the autocompleter
|
|
else
|
|
resetAutocompletion(hash);
|
|
});
|
|
|
|
// Chatstate events
|
|
eventsChatState(inputDetect, room, hash, 'groupchat');
|
|
|
|
// Get the current muc informations and content
|
|
getMUC(room, nickname, password);
|
|
}
|
|
|
|
// Generates a groupchat to join array
|
|
function arrayJoinGroupchats() {
|
|
// Values array
|
|
var muc_arr = [GROUPCHATS_JOIN];
|
|
var new_arr = [];
|
|
|
|
// Try to split it
|
|
if(GROUPCHATS_JOIN.indexOf(',') != -1)
|
|
muc_arr = GROUPCHATS_JOIN.split(',');
|
|
|
|
for(i in muc_arr) {
|
|
// Get the current value
|
|
var muc_current = trim(muc_arr[i]);
|
|
|
|
// No current value?
|
|
if(!muc_current)
|
|
continue;
|
|
|
|
// Filter the current value
|
|
muc_current = generateXID(muc_current, 'groupchat');
|
|
|
|
// Add the current value
|
|
if(!existArrayValue(new_arr, muc_current))
|
|
new_arr.push(muc_current);
|
|
}
|
|
|
|
return new_arr;
|
|
}
|
|
|
|
// Joins the defined groupchats
|
|
var JOIN_SUGGEST = [];
|
|
|
|
function joinConfGroupchats() {
|
|
// Nothing to join?
|
|
if(!JOIN_SUGGEST)
|
|
return;
|
|
|
|
// Join the chats
|
|
if(JOIN_SUGGEST.length) {
|
|
for(g in JOIN_SUGGEST)
|
|
checkChatCreate(JOIN_SUGGEST[g], 'groupchat');
|
|
}
|
|
}
|
|
|
|
// Checks suggest utility
|
|
function suggestCheck() {
|
|
var groupchat_arr = arrayJoinGroupchats();
|
|
|
|
// Must suggest the user?
|
|
if((GROUPCHATS_SUGGEST == 'on') && groupchat_arr.length) {
|
|
if(exists('#suggest'))
|
|
return;
|
|
|
|
// Create HTML code
|
|
var html = '<div id="suggest">';
|
|
html += '<div class="title">' + _e("Suggested chatrooms") + '</div>';
|
|
|
|
html += '<div class="content">';
|
|
for(g in groupchat_arr) {
|
|
html += '<a class="one" href="#" data-xid="' + encodeQuotes(groupchat_arr[g]) + '">';
|
|
html += '<span class="icon talk-images"></span>';
|
|
html += '<span class="name">' + capitaliseFirstLetter(getXIDNick(groupchat_arr[g]).htmlEnc()) + '</span>';
|
|
html += '<span class="state talk-images"></span>';
|
|
html += '<span class="clear"></span>';
|
|
html += '</a>';
|
|
}
|
|
html += '</div>';
|
|
|
|
html += '<a class="next disabled" href="#">' + _e("Continue") + '</a>';
|
|
html += '</div>';
|
|
|
|
// Append HTML code
|
|
$('body').append(html);
|
|
|
|
// Click events
|
|
$('#suggest .content a.one').click(function() {
|
|
// Add/remove the active class
|
|
$(this).toggleClass('active');
|
|
|
|
// We require at least one room to be chosen
|
|
if(exists('#suggest .content a.one.active'))
|
|
$('#suggest a.next').removeClass('disabled');
|
|
else
|
|
$('#suggest a.next').addClass('disabled');
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#suggest a.next').click(function() {
|
|
// Disabled?
|
|
if($(this).hasClass('disabled'))
|
|
return false;
|
|
|
|
// Store groupchats to join
|
|
$('#suggest .content a.one.active').each(function() {
|
|
JOIN_SUGGEST.push($(this).attr('data-xid'));
|
|
});
|
|
|
|
// Switch to talk UI
|
|
$('#suggest').remove();
|
|
triggerConnected();
|
|
|
|
return false;
|
|
});
|
|
} else {
|
|
JOIN_SUGGEST = groupchat_arr;
|
|
|
|
triggerConnected();
|
|
}
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the smileys JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 21/03/11
|
|
|
|
*/
|
|
|
|
// Generates the correct HTML code for an emoticon insertion tool
|
|
function emoteLink(smiley, image, hash) {
|
|
return '<a href="#" class="emoticon emoticon-' + image + ' smileys-images" data-smiley="' + smiley + '"></a>';
|
|
}
|
|
|
|
// Emoticon links arrays
|
|
function smileyLinks(hash) {
|
|
var links = '';
|
|
|
|
var sArray = new Array(
|
|
':-D',
|
|
']:->',
|
|
'8-)',
|
|
':-P',
|
|
':-)',
|
|
';-)',
|
|
':-$',
|
|
':-|',
|
|
':-/',
|
|
'=-O',
|
|
':-(',
|
|
':\'-(',
|
|
':-@',
|
|
':-!',
|
|
'({)',
|
|
'(})',
|
|
':3',
|
|
'(@)',
|
|
':-[',
|
|
':-{}',
|
|
'<3',
|
|
'</3',
|
|
'@}->--',
|
|
'(W)',
|
|
'(Y)',
|
|
'(N)',
|
|
'(I)',
|
|
'(C)',
|
|
'(D)',
|
|
'(B)',
|
|
'(Z)',
|
|
'(X)',
|
|
'(P)',
|
|
'(T)',
|
|
'(8)',
|
|
'(%)',
|
|
'(E)',
|
|
'(R)',
|
|
'(*)',
|
|
'(S)'
|
|
);
|
|
|
|
var cArray = new Array(
|
|
'biggrin',
|
|
'devil',
|
|
'coolglasses',
|
|
'tongue',
|
|
'smile',
|
|
'wink',
|
|
'blush',
|
|
'stare',
|
|
'frowning',
|
|
'oh',
|
|
'unhappy',
|
|
'cry',
|
|
'angry',
|
|
'puke',
|
|
'hugright',
|
|
'hugleft',
|
|
'lion',
|
|
'pussy',
|
|
'bat',
|
|
'kiss',
|
|
'heart',
|
|
'brheart',
|
|
'flower',
|
|
'brflower',
|
|
'thumbup',
|
|
'thumbdown',
|
|
'lamp',
|
|
'coffee',
|
|
'drink',
|
|
'beer',
|
|
'boy',
|
|
'girl',
|
|
'photo',
|
|
'phone',
|
|
'music',
|
|
'cuffs',
|
|
'mail',
|
|
'rainbow',
|
|
'star',
|
|
'moon'
|
|
);
|
|
|
|
for(i in sArray)
|
|
links += emoteLink(sArray[i], cArray[i], hash);
|
|
|
|
return links;
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the Out of Band Data JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 27/08/11
|
|
|
|
*/
|
|
|
|
// Sends an OOB request to someone
|
|
function sendOOB(to, type, url, desc) {
|
|
// IQ stanza?
|
|
if(type == 'iq') {
|
|
// Get some values
|
|
var id = hex_md5(genID() + to + url + desc);
|
|
to = highestPriority(to);
|
|
|
|
// IQs cannot be sent to offline users
|
|
if(!to)
|
|
return;
|
|
|
|
// Register the ID
|
|
setDB('send/url', id, url);
|
|
setDB('send/desc', id, desc);
|
|
|
|
var aIQ = new JSJaCIQ();
|
|
aIQ.setTo(fullXID(to));
|
|
aIQ.setType('set');
|
|
aIQ.setID(id);
|
|
|
|
// Append the query content
|
|
var aQuery = aIQ.setQuery(NS_IQOOB);
|
|
aQuery.appendChild(aIQ.buildNode('url', {'xmlns': NS_IQOOB}, url));
|
|
aQuery.appendChild(aIQ.buildNode('desc', {'xmlns': NS_IQOOB}, desc));
|
|
|
|
con.send(aIQ);
|
|
}
|
|
|
|
// Message stanza?
|
|
else {
|
|
var aMsg = new JSJaCMessage();
|
|
aMsg.setTo(bareXID(to));
|
|
|
|
// Append the content
|
|
aMsg.setBody(desc);
|
|
var aX = aMsg.appendNode('x', {'xmlns': NS_XOOB});
|
|
aX.appendChild(aMsg.buildNode('url', {'xmlns': NS_XOOB}, url));
|
|
|
|
con.send(aMsg);
|
|
}
|
|
|
|
logThis('Sent OOB request to: ' + to + ' (' + desc + ')');
|
|
}
|
|
|
|
// Handles an OOB request
|
|
function handleOOB(from, id, type, node) {
|
|
var xid = url = desc = '';
|
|
|
|
// IQ stanza?
|
|
if(type == 'iq') {
|
|
xid = fullXID(from);
|
|
url = $(node).find('url').text();
|
|
desc = $(node).find('desc').text();
|
|
}
|
|
|
|
// Message stanza?
|
|
else {
|
|
xid = bareXID(from);
|
|
url = $(node).find('url').text();
|
|
desc = $(node).find('body').text();
|
|
}
|
|
|
|
// No desc?
|
|
if(!desc)
|
|
desc = url;
|
|
|
|
// Open a new notification
|
|
if(type && xid && url && desc)
|
|
newNotification('send', xid, [xid, url, type, id, node], desc, hex_md5(xid + url + desc + id));
|
|
}
|
|
|
|
// Replies to an OOB request
|
|
function replyOOB(to, id, choice, type, node) {
|
|
// Not IQ type?
|
|
if(type != 'iq')
|
|
return;
|
|
|
|
// New IQ
|
|
var aIQ = new JSJaCIQ();
|
|
aIQ.setTo(to);
|
|
aIQ.setID(id);
|
|
|
|
// OOB request accepted
|
|
if(choice == 'accept') {
|
|
aIQ.setType('result');
|
|
|
|
logThis('Accepted file request from: ' + to, 3);
|
|
}
|
|
|
|
// OOB request rejected
|
|
else {
|
|
aIQ.setType('error');
|
|
|
|
// Append stanza content
|
|
for(var i = 0; i < node.childNodes.length; i++)
|
|
aIQ.getNode().appendChild(node.childNodes.item(i).cloneNode(true));
|
|
|
|
// Append error content
|
|
var aError = aIQ.appendNode('error', {'xmlns': NS_CLIENT, 'code': '406', 'type': 'modify'});
|
|
aError.appendChild(aIQ.buildNode('not-acceptable', {'xmlns': NS_STANZAS}));
|
|
|
|
logThis('Rejected file request from: ' + to, 3);
|
|
}
|
|
|
|
con.send(aIQ);
|
|
}
|
|
|
|
// Wait event for OOB upload
|
|
function waitUploadOOB() {
|
|
// Append the wait icon
|
|
$('#page-engine .chat-tools-file:not(.mini) .tooltip-subitem *').hide();
|
|
$('#page-engine .chat-tools-file:not(.mini) .tooltip-subitem').append('<div class="wait wait-medium"></div>');
|
|
|
|
// Lock the bubble
|
|
$('#page-engine .chat-tools-file:not(.mini)').addClass('mini');
|
|
}
|
|
|
|
// Success event for OOB upload
|
|
function handleUploadOOB(responseXML) {
|
|
// Data selector
|
|
var dData = $(responseXML).find('jappix');
|
|
|
|
// Get the values
|
|
var fID = dData.find('id').text();
|
|
var fURL = dData.find('url').text();
|
|
var fDesc = dData.find('desc').text();
|
|
|
|
// Get the OOB values
|
|
var oob_has;
|
|
|
|
// No ID provided?
|
|
if(!fID)
|
|
oob_has = ':has(.wait)';
|
|
else
|
|
oob_has = ':has(#oob-upload input[value="' + fID + '"])';
|
|
|
|
var xid = $('#page-engine .page-engine-chan' + oob_has).attr('data-xid');
|
|
var oob_type = $('#page-engine .chat-tools-file' + oob_has).attr('data-oob');
|
|
|
|
// Reset the file send tool
|
|
$('#page-engine .chat-tools-file' + oob_has).removeClass('mini');
|
|
$('#page-engine .bubble-file' + oob_has).remove();
|
|
|
|
// Not available?
|
|
if($('#page-engine .chat-tools-file' + oob_has).is(':hidden') && (oob_type == 'iq')) {
|
|
openThisError(4);
|
|
|
|
// Remove the file we sent
|
|
if(fURL)
|
|
$.get(fURL + '&action=remove');
|
|
}
|
|
|
|
// Everything okay?
|
|
else if(fURL && fDesc && !dData.find('error').size()) {
|
|
// Send the OOB request
|
|
sendOOB(xid, oob_type, fURL, fDesc);
|
|
|
|
// Notify the sender
|
|
newNotification('send_pending', xid, [xid, fURL, oob_type, '', ''], fDesc, hex_md5(fURL + fDesc + fID));
|
|
|
|
logThis('File request sent.', 3);
|
|
}
|
|
|
|
// Upload error?
|
|
else {
|
|
openThisError(4);
|
|
|
|
logThis('Error while sending the file: ' + dData.find('error').text(), 1);
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the avatar JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou, Maranda
|
|
Last revision: 20/02/13
|
|
|
|
*/
|
|
|
|
// Requests the avatar of a given user
|
|
var AVATAR_PENDING = [];
|
|
|
|
function getAvatar(xid, mode, enabled, photo) {
|
|
/* REF: http://xmpp.org/extensions/xep-0153.html */
|
|
|
|
// No need to get the avatar, another process is yet running
|
|
if(existArrayValue(AVATAR_PENDING, xid))
|
|
return false;
|
|
|
|
// Initialize: XML data is in one SQL entry, because some browser are sloooow with SQL requests
|
|
var xml = XMLFromString(getPersistent('global', 'avatar', xid));
|
|
var forced = false;
|
|
|
|
// Retrieving forced?
|
|
if($(xml).find('forced').text() == 'true')
|
|
forced = true;
|
|
|
|
// No avatar in presence
|
|
if(!photo && !forced && enabled == 'true') {
|
|
// Pending marker
|
|
AVATAR_PENDING.push(xid);
|
|
|
|
// Reset the avatar
|
|
resetAvatar(xid, hex_md5(xid));
|
|
|
|
logThis('No avatar for: ' + xid, 2);
|
|
}
|
|
|
|
// Try to catch the avatar
|
|
else {
|
|
// Define some stuffs
|
|
var type = $(xml).find('type').text();
|
|
var binval = $(xml).find('binval').text();
|
|
var checksum = $(xml).find('checksum').text();
|
|
var updated = false;
|
|
|
|
// Process the checksum of the avatar
|
|
if(checksum == photo || photo == 'forget' || forced)
|
|
updated = true;
|
|
|
|
// If the avatar is yet stored and a new retrieving is not needed
|
|
if(mode == 'cache' && type && binval && checksum && updated) {
|
|
// Pending marker
|
|
AVATAR_PENDING.push(xid);
|
|
|
|
// Display the cache avatar
|
|
displayAvatar(xid, hex_md5(xid), type, binval);
|
|
|
|
logThis('Read avatar from cache: ' + xid, 3);
|
|
}
|
|
|
|
// Else if the request has not yet been fired, we get it
|
|
else if((!updated || mode == 'force' || photo == 'forget') && enabled != 'false') {
|
|
// Pending marker
|
|
AVATAR_PENDING.push(xid);
|
|
|
|
// Get the latest avatar
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
iq.setTo(xid);
|
|
|
|
iq.appendNode('vCard', {'xmlns': NS_VCARD});
|
|
|
|
con.send(iq, handleAvatar);
|
|
|
|
logThis('Get avatar from server: ' + xid, 3);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Handles the avatar
|
|
function handleAvatar(iq) {
|
|
// Extract the XML values
|
|
var handleXML = iq.getNode();
|
|
var handleFrom = fullXID(getStanzaFrom(iq));
|
|
|
|
// Is this me? Remove the resource!
|
|
if(bareXID(handleFrom) == getXID())
|
|
handleFrom = bareXID(handleFrom);
|
|
|
|
// Get some other values
|
|
var hash = hex_md5(handleFrom);
|
|
var find = $(handleXML).find('vCard');
|
|
var aChecksum = 'none';
|
|
var oChecksum = null;
|
|
|
|
// Read our own checksum
|
|
if(handleFrom == getXID()) {
|
|
oChecksum = getDB('checksum', 1);
|
|
|
|
// Avoid the "null" value
|
|
if(!oChecksum)
|
|
oChecksum = '';
|
|
}
|
|
|
|
// vCard not empty?
|
|
if(find.size()) {
|
|
// We get our profile details
|
|
if(handleFrom == getXID()) {
|
|
// Get the names
|
|
var names = generateBuddyName(iq);
|
|
|
|
// Write the values to the database
|
|
setDB('profile', 'name', names[0]);
|
|
setDB('profile', 'nick', names[1]);
|
|
}
|
|
|
|
// We get the avatar
|
|
var aType = find.find('TYPE:first').text();
|
|
var aBinval = find.find('BINVAL:first').text();
|
|
|
|
// No binval?
|
|
if(!aBinval) {
|
|
aType = 'none';
|
|
aBinval = 'none';
|
|
}
|
|
|
|
// Enough data
|
|
else {
|
|
// No type?
|
|
if(!aType)
|
|
aType = 'image/png';
|
|
|
|
// Process the checksum
|
|
else
|
|
aChecksum = hex_sha1(Base64.decode(aBinval));
|
|
}
|
|
|
|
// We display the user avatar
|
|
displayAvatar(handleFrom, hash, aType, aBinval);
|
|
|
|
// Store the avatar
|
|
setPersistent('global', 'avatar', handleFrom, '<avatar><type>' + aType + '</type><binval>' + aBinval + '</binval><checksum>' + aChecksum + '</checksum><forced>false</forced></avatar>');
|
|
|
|
logThis('Avatar retrieved from server: ' + handleFrom, 3);
|
|
}
|
|
|
|
// vCard is empty
|
|
else
|
|
resetAvatar(handleFrom);
|
|
|
|
// We got a new checksum for us?
|
|
if(((oChecksum != null) && (oChecksum != aChecksum)) || !FIRST_PRESENCE_SENT) {
|
|
// Define a proper checksum
|
|
var pChecksum = aChecksum;
|
|
|
|
if(pChecksum == 'none')
|
|
pChecksum = '';
|
|
|
|
// Update our temp. checksum
|
|
setDB('checksum', 1, pChecksum);
|
|
|
|
// Send the stanza
|
|
if(!FIRST_PRESENCE_SENT)
|
|
getStorage(NS_OPTIONS);
|
|
else if(hasPersistent())
|
|
presenceSend(pChecksum);
|
|
}
|
|
}
|
|
|
|
// Reset the avatar of an user
|
|
function resetAvatar(xid, hash) {
|
|
// Store the empty avatar
|
|
setPersistent('global', 'avatar', xid, '<avatar><type>none</type><binval>none</binval><checksum>none</checksum><forced>false</forced></avatar>');
|
|
|
|
// Display the empty avatar
|
|
displayAvatar(xid, hash, 'none', 'none');
|
|
}
|
|
|
|
// Displays the avatar of an user
|
|
function displayAvatar(xid, hash, type, binval) {
|
|
// Initialize the vars
|
|
var container = hash + ' .avatar-container';
|
|
var code = '<img class="avatar" src="';
|
|
|
|
// If the avatar exists
|
|
if((type != 'none') && (binval != 'none'))
|
|
code += 'data:' + type + ';base64,' + binval;
|
|
else
|
|
code += './img/others/default-avatar.png';
|
|
|
|
code += '" alt="" />';
|
|
|
|
// Replace with the new avatar (in the roster and in the chat)
|
|
$('.' + container).html(code);
|
|
|
|
// We can remove the pending marker
|
|
removeArrayValue(AVATAR_PENDING, xid);
|
|
}
|
|
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the mucadmin JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 03/03/11
|
|
|
|
*/
|
|
|
|
// Opens the MUC admin popup
|
|
function openMucAdmin(xid, aff) {
|
|
// Popup HTML content
|
|
var html_full =
|
|
'<div class="top">' + _e("MUC administration") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="head mucadmin-head">' +
|
|
'<div class="head-text mucadmin-head-text">' + _e("You administrate this room") + '</div>' +
|
|
|
|
'<div class="mucadmin-head-jid">' + xid + '</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="mucadmin-forms">' +
|
|
'<div class="mucadmin-topic">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Subject") + '</legend>' +
|
|
|
|
'<label for="topic-text">' + _e("Enter new subject") + '</label>' +
|
|
'<textarea id="topic-text" name="room-topic" rows="8" cols="60" ></textarea>' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
|
|
'<div class="mucadmin-conf">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Configuration") + '</legend>' +
|
|
|
|
'<div class="results mucadmin-results"></div>' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
|
|
'<div class="mucadmin-aut">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Authorizations") + '</legend>' +
|
|
|
|
'<label>' + _e("Member list") + '</label>' +
|
|
'<div class="aut-member aut-group">' +
|
|
'<a href="#" class="aut-add" onclick="return addInputMucAdmin(\'\', \'member\');">' + _e("Add an input") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<label>' + _e("Owner list") + '</label>' +
|
|
'<div class="aut-owner aut-group">' +
|
|
'<a href="#" class="aut-add" onclick="return addInputMucAdmin(\'\', \'owner\');">' + _e("Add an input") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<label>' + _e("Administrator list") + '</label>' +
|
|
'<div class="aut-admin aut-group">' +
|
|
'<a href="#" class="aut-add" onclick="return addInputMucAdmin(\'\', \'admin\');">' + _e("Add an input") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<label>' + _e("Outcast list") + '</label>' +
|
|
'<div class="aut-outcast aut-group">' +
|
|
'<a href="#" class="aut-add" onclick="return addInputMucAdmin(\'\', \'outcast\');">' + _e("Add an input") + '</a>' +
|
|
'</div>' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
|
|
'<div class="mucadmin-others">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Others") + '</legend>' +
|
|
|
|
'<label>' + _e("Destroy this MUC") + '</label>' +
|
|
'<a href="#" onclick="return destroyMucAdmin();">' + _e("Yes, let's do it!") + '</a>' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish save">' + _e("Save") + '</a>' +
|
|
'<a href="#" class="finish cancel">' + _e("Cancel") + '</a>' +
|
|
'</div>';
|
|
|
|
var html_partial =
|
|
'<div class="top">' + _e("MUC administration") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="head mucadmin-head">' +
|
|
'<div class="head-text mucadmin-head-text">' + _e("You administrate this room") + '</div>' +
|
|
|
|
'<div class="mucadmin-head-jid">' + xid + '</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="mucadmin-forms">' +
|
|
'<div class="mucadmin-aut">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Authorizations") + '</legend>' +
|
|
|
|
'<label>' + _e("Member list") + '</label>' +
|
|
'<div class="aut-member aut-group">' +
|
|
'<a href="#" class="aut-add" onclick="return addInputMucAdmin(\'\', \'member\');">' + _e("Add an input") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<label>' + _e("Outcast list") + '</label>' +
|
|
'<div class="aut-outcast aut-group">' +
|
|
'<a href="#" class="aut-add" onclick="return addInputMucAdmin(\'\', \'outcast\');">' + _e("Add an input") + '</a>' +
|
|
'</div>' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish save">' + _e("Save") + '</a>' +
|
|
'<a href="#" class="finish cancel">' + _e("Cancel") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
if(aff == 'owner')
|
|
createPopup('mucadmin', html_full);
|
|
if(aff == 'admin')
|
|
createPopup('mucadmin', html_partial);
|
|
|
|
// Associate the events
|
|
launchMucAdmin();
|
|
|
|
// We get the affiliated user's privileges
|
|
if(aff == 'owner') {
|
|
queryMucAdmin(xid, 'member');
|
|
queryMucAdmin(xid, 'owner');
|
|
queryMucAdmin(xid, 'admin');
|
|
queryMucAdmin(xid, 'outcast');
|
|
// We query the room to edit
|
|
dataForm(xid, 'muc', '', '', 'mucadmin');
|
|
} else if(aff == 'admin') {
|
|
queryMucAdmin(xid, 'member');
|
|
queryMucAdmin(xid, 'outcast');
|
|
}
|
|
}
|
|
|
|
// Closes the MUC admin popup
|
|
function closeMucAdmin() {
|
|
// Destroy the popup
|
|
destroyPopup('mucadmin');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Removes a MUC admin input
|
|
function removeInputMucAdmin(element) {
|
|
var path = $(element).parent();
|
|
|
|
// We first hide the container of the input
|
|
path.hide();
|
|
|
|
// Then, we add a special class to the input
|
|
path.find('input').addClass('aut-dustbin');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Adds a MUC admin input
|
|
function addInputMucAdmin(xid, affiliation) {
|
|
var hash = hex_md5(xid + affiliation);
|
|
|
|
// Add the HTML code
|
|
$('#mucadmin .aut-' + affiliation + ' .aut-add').after(
|
|
'<div class="one-aut ' + hash + '">' +
|
|
'<input id="aut-' + affiliation + '" name="' + affiliation + '" type="text" class="mucadmin-i" value="' + xid + '" />' +
|
|
'<a href="#" class="aut-remove">[-]</a>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Click event
|
|
$('#mucadmin .' + hash + ' .aut-remove').click(function() {
|
|
return removeInputMucAdmin(this);
|
|
});
|
|
|
|
// Focus on the input we added
|
|
if(!xid)
|
|
$(document).oneTime(10, function() {
|
|
$('#mucadmin .' + hash + ' input').focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the MUC admin form
|
|
function handleMucAdminAuth(iq) {
|
|
// We got the authorizations results
|
|
$(iq.getQuery()).find('item').each(function() {
|
|
// We parse the received xml
|
|
var xid = $(this).attr('jid');
|
|
var affiliation = $(this).attr('affiliation');
|
|
|
|
// We create one input for one XID
|
|
addInputMucAdmin(xid, affiliation);
|
|
});
|
|
|
|
// Hide the wait icon
|
|
$('#mucadmin .wait').hide();
|
|
|
|
logThis('MUC admin items received: ' + fullXID(getStanzaFrom(iq)));
|
|
}
|
|
|
|
// Queries the MUC admin form
|
|
function queryMucAdmin(xid, type) {
|
|
// Show the wait icon
|
|
$('#mucadmin .wait').show();
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setTo(xid);
|
|
iq.setType('get');
|
|
|
|
var iqQuery = iq.setQuery(NS_MUC_ADMIN);
|
|
iqQuery.appendChild(iq.buildNode('item', {'affiliation': type, 'xmlns': NS_MUC_ADMIN}));
|
|
|
|
con.send(iq, handleMucAdminAuth);
|
|
}
|
|
|
|
// Sends the new chat-room topic
|
|
function sendMucAdminTopic(xid) {
|
|
// We get the new topic
|
|
var topic = $('.mucadmin-topic textarea').val();
|
|
|
|
// We send the new topic if not blank
|
|
if(topic) {
|
|
var m = new JSJaCMessage();
|
|
m.setTo(xid);
|
|
m.setType('groupchat');
|
|
m.setSubject(topic);
|
|
con.send(m);
|
|
|
|
logThis('MUC admin topic sent: ' + topic, 3);
|
|
}
|
|
}
|
|
|
|
// Sends the MUC admin auth form
|
|
function sendMucAdminAuth(xid) {
|
|
// We define the values array
|
|
var types = new Array('member', 'owner', 'admin', 'outcast');
|
|
|
|
for(i in types) {
|
|
// We get the current type
|
|
var tType = types[i];
|
|
|
|
// We loop for all the elements
|
|
$('.mucadmin-aut .aut-' + tType + ' input').each(function() {
|
|
// We set the iq headers
|
|
var iq = new JSJaCIQ();
|
|
iq.setTo(xid);
|
|
iq.setType('set');
|
|
|
|
var iqQuery = iq.setQuery(NS_MUC_ADMIN);
|
|
|
|
// We get the needed values
|
|
var value = $(this).val();
|
|
|
|
// If there's a value
|
|
if(value)
|
|
var item = iqQuery.appendChild(iq.buildNode('item', {'jid': value, 'xmlns': NS_MUC_ADMIN}));
|
|
|
|
// It the user had removed the XID
|
|
if($(this).hasClass('aut-dustbin') && value)
|
|
item.setAttribute('affiliation', 'none');
|
|
|
|
// If the value is not blank and okay
|
|
else if(value)
|
|
item.setAttribute('affiliation', tType);
|
|
|
|
// We send the iq !
|
|
con.send(iq, handleErrorReply);
|
|
});
|
|
}
|
|
|
|
logThis('MUC admin authorizations form sent: ' + xid, 3);
|
|
}
|
|
|
|
// Checks if the MUC room was destroyed
|
|
function handleDestroyMucAdminIQ(iq) {
|
|
if(!handleErrorReply(iq)) {
|
|
// We close the groupchat
|
|
var room = fullXID(getStanzaFrom(iq));
|
|
var hash = hex_md5(room);
|
|
quitThisChat(room, hash, 'groupchat');
|
|
|
|
// We close the muc admin popup
|
|
closeMucAdmin();
|
|
|
|
// We tell the user that all is okay
|
|
openThisInfo(5);
|
|
|
|
// We remove the user's favorite
|
|
if(existDB('favorites', room))
|
|
removeThisFavorite(room, explodeThis('@', room, 0));
|
|
|
|
logThis('MUC admin destroyed: ' + room, 3);
|
|
}
|
|
|
|
// We hide the wait icon
|
|
$('#mucadmin .wait').hide();
|
|
}
|
|
|
|
// Destroys a MUC room
|
|
function destroyMucAdminIQ(xid) {
|
|
// We ask the server to delete the room
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setTo(xid);
|
|
iq.setType('set');
|
|
var iqQuery = iq.setQuery(NS_MUC_OWNER);
|
|
iqQuery.appendChild(iq.buildNode('destroy', {'xmlns': NS_MUC_OWNER}));
|
|
|
|
con.send(iq, handleDestroyMucAdminIQ);
|
|
|
|
logThis('MUC admin destroy sent: ' + xid, 3);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Performs the MUC room destroy functions
|
|
function destroyMucAdmin() {
|
|
// We get the XID of the current room
|
|
var xid = $('#mucadmin .mucadmin-head-jid').text();
|
|
|
|
// We show the wait icon
|
|
$('#mucadmin .wait').show();
|
|
|
|
// We send the iq
|
|
destroyMucAdminIQ(xid);
|
|
}
|
|
|
|
// Sends all the MUC admin stuffs
|
|
function sendMucAdmin() {
|
|
// We get the XID of the current room
|
|
var xid = $('#mucadmin .mucadmin-head-jid').text();
|
|
|
|
// We change the room topic
|
|
sendMucAdminTopic(xid);
|
|
|
|
// We send the needed queries
|
|
sendDataForm('x', 'submit', 'submit', $('#mucadmin .mucadmin-results').attr('data-session'), xid, '', '', 'mucadmin');
|
|
sendMucAdminAuth(xid);
|
|
}
|
|
|
|
// Saves the MUC admin elements
|
|
function saveMucAdmin() {
|
|
// We send the new options
|
|
sendMucAdmin();
|
|
|
|
// And we quit the popup
|
|
return closeMucAdmin();
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchMucAdmin() {
|
|
// Click events
|
|
$('#mucadmin .bottom .finish').click(function() {
|
|
if($(this).is('.cancel'))
|
|
return closeMucAdmin();
|
|
if($(this).is('.save'))
|
|
return saveMucAdmin();
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the connection JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 20/02/12
|
|
|
|
*/
|
|
|
|
// Does the user login
|
|
var CURRENT_SESSION = false;
|
|
|
|
function doLogin(lNick, lServer, lPass, lResource, lPriority, lRemember, loginOpts) {
|
|
try {
|
|
// get optionnal conn handlers
|
|
oExtend = loginOpts || {};
|
|
|
|
// We remove the not completed class to avoid problems
|
|
$('#home .loginer input').removeClass('please-complete');
|
|
|
|
// We add the login wait div
|
|
showGeneralWait();
|
|
|
|
// We define the http binding parameters
|
|
oArgs = new Object();
|
|
|
|
if(HOST_BOSH_MAIN)
|
|
oArgs.httpbase = HOST_BOSH_MAIN;
|
|
else
|
|
oArgs.httpbase = HOST_BOSH;
|
|
|
|
// Check BOSH origin
|
|
BOSH_SAME_ORIGIN = isSameOrigin(oArgs.httpbase);
|
|
|
|
// We create the new http-binding connection
|
|
con = new JSJaCHttpBindingConnection(oArgs);
|
|
|
|
// And we handle everything that happen
|
|
setupCon(con,oExtend);
|
|
|
|
// Generate a resource
|
|
var random_resource = getDB('session', 'resource');
|
|
|
|
if(!random_resource)
|
|
random_resource = lResource + ' (' + (new Date()).getTime() + ')';
|
|
|
|
// We retrieve what the user typed in the login inputs
|
|
oArgs = new Object();
|
|
oArgs.domain = trim(lServer);
|
|
oArgs.username = trim(lNick);
|
|
oArgs.resource = random_resource;
|
|
oArgs.pass = lPass;
|
|
oArgs.secure = true;
|
|
oArgs.xmllang = XML_LANG;
|
|
|
|
// Store the resource (for reconnection)
|
|
setDB('session', 'resource', random_resource);
|
|
|
|
// Store session XML in temporary database
|
|
storeSession(lNick, lServer, lPass, lResource, lPriority, lRemember);
|
|
|
|
// We store the infos of the user into the data-base
|
|
setDB('priority', 1, lPriority);
|
|
|
|
// We connect !
|
|
con.connect(oArgs);
|
|
|
|
// Change the page title
|
|
pageTitle('wait');
|
|
|
|
logThis('Jappix is connecting...', 3);
|
|
}
|
|
|
|
catch(e) {
|
|
// Logs errors
|
|
logThis('Error while logging in: ' + e, 1);
|
|
|
|
// Reset Jappix
|
|
destroyTalkPage();
|
|
|
|
// Open an unknown error
|
|
openThisError(2);
|
|
}
|
|
|
|
finally {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Handles the user registration
|
|
function handleRegistered() {
|
|
logThis('A new account has been registered.', 3);
|
|
|
|
// We remove the waiting image
|
|
removeGeneralWait();
|
|
|
|
// Reset the title
|
|
pageTitle('home');
|
|
|
|
// We show the success information
|
|
$('#home .registerer .success').fadeIn('fast');
|
|
|
|
// We quit the session
|
|
if(isConnected())
|
|
logout();
|
|
}
|
|
|
|
// Does the user registration
|
|
function doRegister(username, domain, pass, captcha) {
|
|
logThis('Trying to register an account...', 3);
|
|
|
|
// We change the registered information text
|
|
$('#home .homediv.registerer').append(
|
|
'<div class="info success">' +
|
|
_e("You have been registered, here is your XMPP address:") + ' <b>' + username.htmlEnc() + '@' + domain.htmlEnc() + '</b> - <a href="#">' + _e("Login") + '</a>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Login link
|
|
$('#home .homediv.registerer .success a').click(function() {
|
|
return doLogin(username, domain, pass, '', '10', false);
|
|
});
|
|
|
|
if((REGISTER_API == 'on') && (domain == HOST_MAIN) && captcha) {
|
|
// Show the waiting image
|
|
showGeneralWait();
|
|
|
|
// Change the page title
|
|
pageTitle('wait');
|
|
|
|
// Send request
|
|
$.post('./php/register.php', {username: username, domain: domain, password: pass, captcha: captcha}, function(data) {
|
|
// Error registering
|
|
removeGeneralWait();
|
|
pageTitle('home');
|
|
|
|
// In all case, update CAPTCHA
|
|
$('#home img.captcha_img').attr('src', './php/captcha.php?id=' + genID());
|
|
$('#home input.captcha').val('');
|
|
|
|
// Registration okay
|
|
if($(data).find('query status').text() == '1') {
|
|
handleRegistered();
|
|
} else {
|
|
// Show error message
|
|
var error_message = '';
|
|
|
|
switch($(data).find('query message').text()) {
|
|
case 'CAPTCHA Not Matching':
|
|
error_message = _e("The security code you entered is invalid. Please retry with another one.");
|
|
|
|
$('#home input.captcha').focus();
|
|
|
|
break;
|
|
|
|
case 'Username Unavailable':
|
|
error_message = _e("The username you picked is not available. Please try another one.");
|
|
|
|
$('#home input.nick').focus();
|
|
|
|
break;
|
|
|
|
default:
|
|
error_message = _e("There was an error registering your account. Please retry.");
|
|
|
|
break;
|
|
}
|
|
|
|
if(error_message)
|
|
showError('', error_message, '');
|
|
}
|
|
});
|
|
} else {
|
|
try {
|
|
// We define the http binding parameters
|
|
oArgs = new Object();
|
|
|
|
if(HOST_BOSH_MAIN)
|
|
oArgs.httpbase = HOST_BOSH_MAIN;
|
|
else
|
|
oArgs.httpbase = HOST_BOSH;
|
|
|
|
// Check BOSH origin
|
|
BOSH_SAME_ORIGIN = isSameOrigin(oArgs.httpbase);
|
|
|
|
// We create the new http-binding connection
|
|
con = new JSJaCHttpBindingConnection(oArgs);
|
|
|
|
// We setup the connection !
|
|
con.registerHandler('onconnect', handleRegistered);
|
|
con.registerHandler('onerror', handleError);
|
|
|
|
// We retrieve what the user typed in the register inputs
|
|
oArgs = new Object();
|
|
oArgs.domain = trim(domain);
|
|
oArgs.username = trim(username);
|
|
oArgs.resource = JAPPIX_RESOURCE + ' Register (' + (new Date()).getTime() + ')';
|
|
oArgs.pass = pass;
|
|
oArgs.register = true;
|
|
oArgs.secure = true;
|
|
oArgs.xmllang = XML_LANG;
|
|
|
|
con.connect(oArgs);
|
|
|
|
// Show the waiting image
|
|
showGeneralWait();
|
|
|
|
// Change the page title
|
|
pageTitle('wait');
|
|
}
|
|
|
|
catch(e) {
|
|
// Logs errors
|
|
logThis(e, 1);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Does the user anonymous login
|
|
function doAnonymous() {
|
|
logThis('Trying to login anonymously...', 3);
|
|
|
|
var aPath = '#home .anonymouser ';
|
|
var room = $(aPath + '.room').val();
|
|
var nick = $(aPath + '.nick').val();
|
|
|
|
// If the form is correctly completed
|
|
if(room && nick) {
|
|
// We remove the not completed class to avoid problems
|
|
$('#home .anonymouser input').removeClass('please-complete');
|
|
|
|
// Redirect the user to the anonymous room
|
|
window.location.href = JAPPIX_LOCATION + '?r=' + room + '&n=' + nick;
|
|
}
|
|
|
|
// We check if the form is entirely completed
|
|
else {
|
|
$(aPath + 'input[type="text"]').each(function() {
|
|
var select = $(this);
|
|
|
|
if(!select.val())
|
|
$(document).oneTime(10, function() {
|
|
select.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
select.removeClass('please-complete');
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the user connected event
|
|
var CONNECTED = false;
|
|
|
|
function handleConnected() {
|
|
logThis('Jappix is now connected.', 3);
|
|
|
|
// Connection markers
|
|
CONNECTED = true;
|
|
RECONNECT_TRY = 0;
|
|
RECONNECT_TIMER = 0;
|
|
|
|
// We hide the home page
|
|
$('#home').hide();
|
|
|
|
// Any suggest to do before triggering connected event?
|
|
suggestCheck();
|
|
|
|
// Remove the waiting item
|
|
removeGeneralWait();
|
|
}
|
|
|
|
// Triggers the connected state
|
|
function triggerConnected() {
|
|
try {
|
|
// Not resumed?
|
|
if(!RESUME) {
|
|
// Remember the session?
|
|
if(getDB('remember', 'session'))
|
|
setPersistent('global', 'session', 1, CURRENT_SESSION);
|
|
|
|
// We show the chatting app.
|
|
createTalkPage();
|
|
|
|
// We reset the homepage
|
|
switchHome('default');
|
|
|
|
// We get all the other things
|
|
getEverything();
|
|
|
|
// Set last activity stamp
|
|
LAST_ACTIVITY = getTimeStamp();
|
|
}
|
|
|
|
// Resumed
|
|
else {
|
|
// Send our presence
|
|
presenceSend();
|
|
|
|
// Change the title
|
|
updateTitle();
|
|
}
|
|
} catch(e) {
|
|
logThis('Error in triggerConnected() with message: ' + e, 1);
|
|
}
|
|
}
|
|
|
|
// Handles the user disconnected event
|
|
function handleDisconnected() {
|
|
logThis('Jappix is now disconnected.', 3);
|
|
|
|
// Normal disconnection
|
|
if(!CURRENT_SESSION && !CONNECTED)
|
|
destroyTalkPage();
|
|
}
|
|
|
|
// Setups the normal connection
|
|
function setupCon(con, oExtend) {
|
|
// Setup connection handlers
|
|
con.registerHandler('message', handleMessage);
|
|
con.registerHandler('presence', handlePresence);
|
|
con.registerHandler('iq', handleIQ);
|
|
con.registerHandler('onconnect', handleConnected);
|
|
con.registerHandler('onerror', handleError);
|
|
con.registerHandler('ondisconnect', handleDisconnected);
|
|
|
|
// Extended handlers
|
|
oExtend = oExtend || {};
|
|
|
|
jQuery.each(oExtend, function(keywd,funct) {
|
|
con.registerHandler(keywd, funct);
|
|
});
|
|
}
|
|
|
|
// Logouts from the server
|
|
function logout() {
|
|
// We are not connected
|
|
if(!isConnected())
|
|
return false;
|
|
|
|
// Disconnect from the XMPP server
|
|
con.disconnect();
|
|
|
|
logThis('Jappix is disconnecting...', 3);
|
|
}
|
|
|
|
// Terminates a session
|
|
function terminate() {
|
|
if(!isConnected())
|
|
return;
|
|
|
|
// Clear temporary session storage
|
|
resetConMarkers();
|
|
|
|
// Show the waiting item (useful if BOSH is sloooow)
|
|
showGeneralWait();
|
|
|
|
// Change the page title
|
|
pageTitle('wait');
|
|
|
|
// Disconnect from the XMPP server
|
|
logout();
|
|
}
|
|
|
|
// Quitss a session
|
|
function quit() {
|
|
if(!isConnected())
|
|
return;
|
|
|
|
// We show the waiting image
|
|
showGeneralWait();
|
|
|
|
// Change the page title
|
|
pageTitle('wait');
|
|
|
|
// We disconnect from the XMPP server
|
|
logout();
|
|
}
|
|
|
|
// Creates the reconnect pane
|
|
var RECONNECT_TRY = 0;
|
|
var RECONNECT_TIMER = 0;
|
|
|
|
function createReconnect(mode) {
|
|
logThis('This is not a normal disconnection, show the reconnect pane...', 1);
|
|
|
|
// Reconnect pane not yet displayed?
|
|
if(!exists('#reconnect')) {
|
|
// Blur the focused input/textarea/select
|
|
$('input, select, textarea').blur();
|
|
|
|
// Create the HTML code
|
|
var html = '<div id="reconnect" class="lock">' +
|
|
'<div class="pane">' +
|
|
_e("Due to a network issue, you were disconnected. What do you want to do now?");
|
|
|
|
// Can we cancel reconnection?
|
|
if(mode == 'normal')
|
|
html += '<a href="#" class="finish cancel">' + _e("Cancel") + '</a>';
|
|
|
|
html += '<a href="#" class="finish reconnect">' + _e("Reconnect") + '</a>' +
|
|
'</div></div>';
|
|
|
|
// Append the code
|
|
$('body').append(html);
|
|
|
|
// Click events
|
|
if(mode == 'normal')
|
|
$('#reconnect a.finish.cancel').click(function() {
|
|
return cancelReconnect();
|
|
});
|
|
|
|
$('#reconnect a.finish.reconnect').click(function() {
|
|
return acceptReconnect(mode);
|
|
});
|
|
|
|
// Try to reconnect automatically after a while
|
|
if(RECONNECT_TRY < 5)
|
|
RECONNECT_TIMER = 5 + (5 * RECONNECT_TRY);
|
|
else
|
|
RECONNECT_TIMER = 120;
|
|
|
|
// Change the try number
|
|
RECONNECT_TRY++;
|
|
|
|
// Fire the event!
|
|
$('#reconnect a.finish.reconnect').everyTime('1s', function() {
|
|
// We can reconnect!
|
|
if(RECONNECT_TIMER == 0)
|
|
return acceptReconnect(mode);
|
|
|
|
// Button text
|
|
if(RECONNECT_TIMER <= 10)
|
|
$(this).text(_e("Reconnect") + ' (' + RECONNECT_TIMER + ')');
|
|
|
|
// Remove 1 second
|
|
RECONNECT_TIMER--;
|
|
});
|
|
|
|
// Page title
|
|
updateTitle();
|
|
}
|
|
}
|
|
|
|
// Reconnects the user if he was disconnected (network issue)
|
|
var RESUME = false;
|
|
|
|
function acceptReconnect(mode) {
|
|
logThis('Trying to reconnect the user...', 3);
|
|
|
|
// Resume marker
|
|
RESUME = true;
|
|
|
|
// Show waiting item
|
|
showGeneralWait();
|
|
|
|
// Reset some various stuffs
|
|
var groupchats = '#page-engine .page-engine-chan[data-type="groupchat"]';
|
|
$(groupchats + ' .list .role').hide();
|
|
$(groupchats + ' .one-group, ' + groupchats + ' .list .user').remove();
|
|
$(groupchats).attr('data-initial', 'false');
|
|
|
|
// Stop the timer
|
|
$('#reconnect a.finish.reconnect').stopTime();
|
|
|
|
// Remove the reconnect pane
|
|
$('#reconnect').remove();
|
|
|
|
// Try to login again
|
|
if(mode == 'normal')
|
|
loginFromSession(XMLFromString(CURRENT_SESSION));
|
|
else if(mode == 'anonymous')
|
|
anonymousLogin(HOST_ANONYMOUS);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Cancel the reconnection of user account (network issue)
|
|
function cancelReconnect() {
|
|
logThis('User has canceled automatic reconnection...', 3);
|
|
|
|
// Stop the timer
|
|
$('#reconnect a.finish.reconnect').stopTime();
|
|
|
|
// Remove the reconnect pane
|
|
$('#reconnect').remove();
|
|
|
|
// Destroy the talk page
|
|
destroyTalkPage();
|
|
|
|
// Renitialize the previous session parameters
|
|
resetConMarkers();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Clears session reminder database
|
|
function clearLastSession() {
|
|
// Clear temporary storage
|
|
resetConMarkers();
|
|
|
|
// Clear persistent storage
|
|
if($(XMLFromString(getPersistent('global', 'session', 1))).find('stored').text() == 'true')
|
|
removePersistent('global', 'session', 1);
|
|
}
|
|
|
|
// Resets the connection markers
|
|
function resetConMarkers() {
|
|
CURRENT_SESSION = false;
|
|
CONNECTED = false;
|
|
RESUME = false;
|
|
RECONNECT_TRY = 0;
|
|
RECONNECT_TIMER = 0;
|
|
}
|
|
|
|
// Logins from a saved session
|
|
function loginFromSession(data) {
|
|
// Select the data
|
|
var session = $(data);
|
|
|
|
// Fire the login event
|
|
doLogin(
|
|
session.find('username').text(),
|
|
session.find('domain').text(),
|
|
session.find('password').text(),
|
|
session.find('resource').text(),
|
|
session.find('priority').text(),
|
|
false
|
|
);
|
|
}
|
|
|
|
// Quits a session normally
|
|
function normalQuit() {
|
|
// Reset our database
|
|
clearLastSession();
|
|
|
|
// We quit the current session
|
|
quit();
|
|
|
|
// We show an info
|
|
openThisInfo(3);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets all the users stuffs
|
|
function getEverything() {
|
|
getFeatures();
|
|
getRoster();
|
|
listPrivacy();
|
|
getStorage(NS_ROSTERNOTES);
|
|
}
|
|
|
|
// Generates session data to store
|
|
function storeSession(lNick, lServer, lPass, lResource, lPriority, lRemember) {
|
|
// Generate a session XML to be stored
|
|
session_xml = '<session><stored>true</stored><domain>' + lServer.htmlEnc() + '</domain><username>' + lNick.htmlEnc() + '</username><resource>' + lResource.htmlEnc() + '</resource><password>' + lPass.htmlEnc() + '</password><priority>' + lPriority.htmlEnc() + '</priority></session>';
|
|
|
|
// Save the session parameters (for reconnect if network issue)
|
|
CURRENT_SESSION = session_xml;
|
|
|
|
// Remember me?
|
|
if(lRemember)
|
|
setDB('remember', 'session', 1);
|
|
|
|
return session_xml;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchConnection() {
|
|
// Logouts when Jappix is closed
|
|
$(window).bind('beforeunload', terminate);
|
|
|
|
// Nothing to do when anonymous!
|
|
if(isAnonymous())
|
|
return;
|
|
|
|
// Connection params submitted in URL?
|
|
if(LINK_VARS['u'] && LINK_VARS['q']) {
|
|
// Generate login data
|
|
var login_xid = bareXID(generateXID(LINK_VARS['u'], 'chat'));
|
|
var login_nick = getXIDNick(login_xid);
|
|
var login_server = getXIDHost(login_xid);
|
|
var login_pwd = LINK_VARS['q'];
|
|
var login_resource = JAPPIX_RESOURCE + ' (' + (new Date()).getTime() + ')';
|
|
var login_priority = '10';
|
|
var login_remember = 1;
|
|
|
|
// Must store session?
|
|
if(LINK_VARS['h'] && (LINK_VARS['h'] == '1')) {
|
|
// Store session
|
|
var session_xml = storeSession(login_nick, login_server, login_pwd, login_resource, login_priority, true);
|
|
setPersistent('global', 'session', 1, session_xml);
|
|
|
|
// Redirect to a clean URL
|
|
document.location.href = './';
|
|
} else {
|
|
// Hide the homepage
|
|
$('#home').hide();
|
|
|
|
// Show the waiting icon
|
|
showGeneralWait();
|
|
|
|
// Proceed login
|
|
doLogin(login_nick, login_server, login_pwd, login_resource, login_priority, login_remember);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Try to resume a stored session, if not anonymous
|
|
var session = XMLFromString(getPersistent('global', 'session', 1));
|
|
|
|
if($(session).find('stored').text() == 'true') {
|
|
// Hide the homepage
|
|
$('#home').hide();
|
|
|
|
// Show the waiting icon
|
|
showGeneralWait();
|
|
|
|
// Login!
|
|
loginFromSession(session);
|
|
|
|
logThis('Saved session found, resuming it...', 3);
|
|
}
|
|
|
|
// Not connected, maybe a XMPP link is submitted?
|
|
else if((parent.location.hash != '#OK') && LINK_VARS['x']) {
|
|
switchHome('loginer');
|
|
|
|
logThis('A XMPP link is set, switch to login page.', 3);
|
|
}
|
|
}
|
|
|
|
// Launch this plugin!
|
|
$(document).ready(launchConnection);
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the dataform JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 19/09/12
|
|
|
|
*/
|
|
|
|
// Gets the defined dataform elements
|
|
function dataForm(host, type, node, action, target) {
|
|
// Clean the current session
|
|
cleanDataForm(target);
|
|
|
|
// We tell the user that a search has been launched
|
|
$('#' + target + ' .wait').show();
|
|
|
|
// If we have enough data
|
|
if(host && type) {
|
|
// Generate a session ID
|
|
var sessionID = Math.round(100000.5 + (((900000.49999) - (100000.5)) * Math.random()));
|
|
var id = target + '-' + sessionID + '-' + genID();
|
|
$('.' + target + '-results').attr('data-session', target + '-' + sessionID);
|
|
|
|
// We request the service item
|
|
var iq = new JSJaCIQ();
|
|
iq.setID(id);
|
|
iq.setTo(host);
|
|
iq.setType('get');
|
|
|
|
// MUC admin query
|
|
if(type == 'muc') {
|
|
iq.setQuery(NS_MUC_OWNER);
|
|
con.send(iq, handleDataFormMuc);
|
|
}
|
|
|
|
// Browse query
|
|
else if(type == 'browse') {
|
|
var iqQuery = iq.setQuery(NS_DISCO_ITEMS);
|
|
|
|
if(node)
|
|
iqQuery.setAttribute('node', node);
|
|
|
|
con.send(iq, handleDataFormBrowse);
|
|
}
|
|
|
|
// Command
|
|
else if(type == 'command') {
|
|
var items;
|
|
|
|
if(node)
|
|
items = iq.appendNode('command', {'node': node, 'xmlns': NS_COMMANDS});
|
|
|
|
else {
|
|
items = iq.setQuery(NS_DISCO_ITEMS);
|
|
items.setAttribute('node', NS_COMMANDS);
|
|
}
|
|
|
|
if(action && node) {
|
|
iq.setType('set');
|
|
items.setAttribute('action', action);
|
|
}
|
|
|
|
con.send(iq, handleDataFormCommand);
|
|
}
|
|
|
|
// Search query
|
|
else if(type == 'search') {
|
|
iq.setQuery(NS_SEARCH);
|
|
con.send(iq, handleDataFormSearch);
|
|
}
|
|
|
|
// Subscribe query
|
|
else if(type == 'subscribe') {
|
|
iq.setQuery(NS_REGISTER);
|
|
con.send(iq, handleDataFormSubscribe);
|
|
}
|
|
|
|
// Join
|
|
else if(type == 'join') {
|
|
if(target == 'discovery')
|
|
closeDiscovery();
|
|
|
|
checkChatCreate(host, 'groupchat');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Sends a given dataform
|
|
function sendDataForm(type, action, x_type, id, xid, node, sessionid, target) {
|
|
// Path
|
|
var pathID = '#' + target + ' .results[data-session="' + id + '"]';
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setTo(xid);
|
|
iq.setType('set');
|
|
|
|
// Set the correct query
|
|
var query;
|
|
|
|
if(type == 'subscribe')
|
|
iqQuery = iq.setQuery(NS_REGISTER);
|
|
else if(type == 'search')
|
|
iqQuery = iq.setQuery(NS_SEARCH);
|
|
else if(type == 'command')
|
|
iqQuery = iq.appendNode('command', {'xmlns': NS_COMMANDS, 'node': node, 'sessionid': sessionid, 'action': action});
|
|
else if(type == 'x')
|
|
iqQuery = iq.setQuery(NS_MUC_OWNER);
|
|
|
|
// Build the XML document
|
|
if(action != 'cancel') {
|
|
// No X node
|
|
if(exists('input.register-special') && (type == 'subscribe')) {
|
|
$('input.register-special').each(function() {
|
|
var iName = $(this).attr('name');
|
|
var iValue = $(this).val();
|
|
|
|
iqQuery.appendChild(iq.buildNode(iName, {'xmlns': NS_REGISTER}, iValue));
|
|
});
|
|
}
|
|
|
|
// Can create the X node
|
|
else {
|
|
var iqX = iqQuery.appendChild(iq.buildNode('x', {'xmlns': NS_XDATA, 'type': x_type}));
|
|
|
|
// Each input
|
|
$(pathID + ' .oneresult input, ' + pathID + ' .oneresult textarea, ' + pathID + ' .oneresult select').each(function() {
|
|
// Get the current input value
|
|
var iVar = $(this).attr('name');
|
|
var iType = $(this).attr('data-type');
|
|
var iValue = $(this).val();
|
|
|
|
// Build a new field node
|
|
var field = iqX.appendChild(iq.buildNode('field', {'var': iVar, 'type': iType, 'xmlns': NS_XDATA}));
|
|
|
|
// Boolean input?
|
|
if(iType == 'boolean') {
|
|
if($(this).filter(':checked').size())
|
|
iValue = '1';
|
|
else
|
|
iValue = '0';
|
|
}
|
|
|
|
// JID-multi input?
|
|
if(iType == 'jid-multi') {
|
|
// Values array
|
|
var xid_arr = [iValue];
|
|
var xid_check = [];
|
|
|
|
// Try to split it
|
|
if(iValue.indexOf(',') != -1)
|
|
xid_arr = iValue.split(',');
|
|
|
|
// Append each value to the XML document
|
|
for(i in xid_arr) {
|
|
// Get the current value
|
|
xid_current = trim(xid_arr[i]);
|
|
|
|
// No current value?
|
|
if(!xid_current)
|
|
continue;
|
|
|
|
// Add the current value
|
|
if(!existArrayValue(xid_check, xid_current)) {
|
|
xid_check.push(xid_current);
|
|
field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, xid_current));
|
|
}
|
|
}
|
|
}
|
|
|
|
// List-multi selector?
|
|
else if(iType == 'list-multi') {
|
|
// Any value?
|
|
if(iValue && iValue.length) {
|
|
for(i in iValue)
|
|
field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue[i]));
|
|
}
|
|
}
|
|
|
|
// Other inputs?
|
|
else
|
|
field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Clean the current session
|
|
cleanDataForm(target);
|
|
|
|
// Show the waiting item
|
|
$('#' + target + ' .wait').show();
|
|
|
|
// Change the ID of the current discovered item
|
|
var iqID = target + '-' + genID();
|
|
$('#' + target + ' .' + target + '-results').attr('data-session', iqID);
|
|
iq.setID(iqID);
|
|
|
|
// Send the IQ
|
|
if(type == 'subscribe')
|
|
con.send(iq, handleDataFormSubscribe);
|
|
else if(type == 'search')
|
|
con.send(iq, handleDataFormSearch);
|
|
else if(type == 'command')
|
|
con.send(iq, handleDataFormCommand);
|
|
else
|
|
con.send(iq);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Displays the good dataform buttons
|
|
function buttonsDataForm(type, action, id, xid, node, sessionid, target, pathID) {
|
|
// No need to use buttons?
|
|
if(type == 'muc')
|
|
return;
|
|
|
|
// Override the "undefined" output
|
|
if(!id)
|
|
id = '';
|
|
if(!xid)
|
|
xid = '';
|
|
if(!node)
|
|
node = '';
|
|
if(!sessionid)
|
|
sessionid = '';
|
|
|
|
// We generate the buttons code
|
|
var buttonsCode = '<div class="oneresult ' + target + '-oneresult ' + target + '-formtools">';
|
|
|
|
if(action == 'submit') {
|
|
if((target == 'adhoc') && (type == 'command')) {
|
|
buttonsCode += '<a href="#" class="submit" onclick="return sendDataForm(\'' + encodeOnclick(type) + '\', \'execute\', \'submit\', \'' + encodeOnclick(id) + '\', \'' + encodeOnclick(xid) + '\', \'' + encodeOnclick(node) + '\', \'' + encodeOnclick(sessionid) + '\', \'' + encodeOnclick(target) + '\');">' + _e("Submit") + '</a>';
|
|
|
|
// When keyup on one text input
|
|
$(pathID + ' input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
sendDataForm(type, 'execute', 'submit', id, xid, node, sessionid, target);
|
|
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
else {
|
|
buttonsCode += '<a href="#" class="submit" onclick="return sendDataForm(\'' + encodeOnclick(type) + '\', \'submit\', \'submit\', \'' + encodeOnclick(id) + '\', \'' + encodeOnclick(xid) + '\', \'' + encodeOnclick(node) + '\', \'' + encodeOnclick(sessionid) + '\', \'' + encodeOnclick(target) + '\');">' + _e("Submit") + '</a>';
|
|
|
|
// When keyup on one text input
|
|
$(pathID + ' input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
sendDataForm(type, 'submit', 'submit', id, xid, node, sessionid, target);
|
|
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if((action == 'submit') && (type != 'subscribe') && (type != 'search'))
|
|
buttonsCode += '<a href="#" class="submit" onclick="return sendDataForm(\'' + encodeOnclick(type) + '\', \'cancel\', \'cancel\', \'' + encodeOnclick(id) + '\', \'' + encodeOnclick(xid) + '\', \'' + encodeOnclick(node) + '\', \'' + encodeOnclick(sessionid) + '\', \'' + encodeOnclick(target) + '\');">' + _e("Cancel") + '</a>';
|
|
|
|
if(((action == 'back') || (type == 'subscribe') || (type == 'search')) && (target == 'discovery'))
|
|
buttonsCode += '<a href="#" class="back" onclick="return startDiscovery();">' + _e("Close") + '</a>';
|
|
|
|
if((action == 'back') && ((target == 'welcome') || (target == 'directory')))
|
|
buttonsCode += '<a href="#" class="back" onclick="return dataForm(HOST_VJUD, \'search\', \'\', \'\', \'' + target + '\');">' + _e("Previous") + '</a>';
|
|
|
|
if((action == 'back') && (target == 'adhoc'))
|
|
buttonsCode += '<a href="#" class="back" onclick="return dataForm(\'' + encodeOnclick(xid) + '\', \'command\', \'\', \'\', \'adhoc\');">' + _e("Previous") + '</a>';
|
|
|
|
buttonsCode += '</div>';
|
|
|
|
// We display the buttons code
|
|
$(pathID).append(buttonsCode);
|
|
|
|
// If no submit link, lock the form
|
|
if(!exists(pathID + ' a.submit'))
|
|
$(pathID + ' input, ' + pathID + ' textarea').attr('readonly', true);
|
|
}
|
|
|
|
// Handles the MUC dataform
|
|
function handleDataFormMuc(iq) {
|
|
handleErrorReply(iq);
|
|
handleDataFormContent(iq, 'muc');
|
|
}
|
|
|
|
// Handles the browse dataform
|
|
function handleDataFormBrowse(iq) {
|
|
handleErrorReply(iq);
|
|
handleDataFormContent(iq, 'browse');
|
|
}
|
|
|
|
// Handles the command dataform
|
|
function handleDataFormCommand(iq) {
|
|
handleErrorReply(iq);
|
|
handleDataFormContent(iq, 'command');
|
|
}
|
|
|
|
// Handles the subscribe dataform
|
|
function handleDataFormSubscribe(iq) {
|
|
handleErrorReply(iq);
|
|
handleDataFormContent(iq, 'subscribe');
|
|
}
|
|
|
|
// Handles the search dataform
|
|
function handleDataFormSearch(iq) {
|
|
handleErrorReply(iq);
|
|
handleDataFormContent(iq, 'search');
|
|
}
|
|
|
|
// Handles the dataform content
|
|
function handleDataFormContent(iq, type) {
|
|
// Get the ID
|
|
var sID = iq.getID();
|
|
|
|
// Get the target
|
|
var splitted = sID.split('-');
|
|
var target = splitted[0];
|
|
var sessionID = target + '-' + splitted[1];
|
|
var from = fullXID(getStanzaFrom(iq));
|
|
var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]';
|
|
|
|
// If an error occured
|
|
if(!iq || (iq.getType() != 'result'))
|
|
noResultDataForm(pathID);
|
|
|
|
// If we got something okay
|
|
else {
|
|
var handleXML = iq.getNode();
|
|
|
|
if(type == 'browse') {
|
|
if($(handleXML).find('item').attr('jid')) {
|
|
// Get the query node
|
|
var queryNode = $(handleXML).find('query').attr('node');
|
|
|
|
$(handleXML).find('item').each(function() {
|
|
// We parse the received xml
|
|
var itemHost = $(this).attr('jid');
|
|
var itemNode = $(this).attr('node');
|
|
var itemName = $(this).attr('name');
|
|
var itemHash = hex_md5(itemHost);
|
|
|
|
// Node
|
|
if(itemNode)
|
|
$(pathID).append(
|
|
'<div class="oneresult ' + target + '-oneresult" onclick="return dataForm(\'' + encodeOnclick(itemHost) + '\', \'browse\', \'' + encodeOnclick(itemNode) + '\', \'\', \'' + encodeOnclick(target) + '\');">' +
|
|
'<div class="one-name">' + itemNode.htmlEnc() + '</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Item
|
|
else if(queryNode && itemName)
|
|
$(pathID).append(
|
|
'<div class="oneresult ' + target + '-oneresult">' +
|
|
'<div class="one-name">' + itemName.htmlEnc() + '</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Item with children
|
|
else {
|
|
// We display the waiting element
|
|
$(pathID + ' .disco-wait .disco-category-title').after(
|
|
'<div class="oneresult ' + target + '-oneresult ' + itemHash + '">' +
|
|
'<div class="one-icon loading talk-images"></div>' +
|
|
'<div class="one-host">' + itemHost + '</div>' +
|
|
'<div class="one-type">' + _e("Requesting this service...") + '</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// We display the category
|
|
$('#' + target + ' .disco-wait').show();
|
|
|
|
// We ask the server what's the service type
|
|
getDataFormType(itemHost, itemNode, sessionID);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Else, there are no items for this query
|
|
else
|
|
noResultDataForm(pathID);
|
|
}
|
|
|
|
else if((type == 'muc') || (type == 'search') || (type == 'subscribe') || ((type == 'command') && $(handleXML).find('command').attr('xmlns'))) {
|
|
// Get some values
|
|
var xCommand = $(handleXML).find('command');
|
|
var bNode = xCommand.attr('node');
|
|
var bSession = xCommand.attr('sessionid');
|
|
var bStatus = xCommand.attr('status');
|
|
var xRegister = $(handleXML).find('query[xmlns="' + NS_REGISTER + '"]').text();
|
|
var xElement = $(handleXML).find('x');
|
|
|
|
// Search done
|
|
if((xElement.attr('type') == 'result') && (type == 'search')) {
|
|
var bPath = pathID;
|
|
|
|
// Display the result
|
|
$(handleXML).find('item').each(function() {
|
|
// Have some "flexibility" for what regards field names, it would be better to return the whole original DF
|
|
// layout, but on a large amount of result which have many fields, there's a very high chance the browser can
|
|
// choke on old systems or new ones even.
|
|
|
|
// Search for useful fields, return first result. This is rather hacky, but jQuery is horrible when it comes to
|
|
// matching st. using patterns. (TODO: Improve and return the full DF layout without choking the browser)
|
|
var bName;
|
|
var bCountry;
|
|
var doneName, doneCountry;
|
|
|
|
$.each($(this).find('field'), function(i, item)
|
|
{
|
|
var $item = $(item);
|
|
if ($(item).attr('var').match(/^(fn|name|[^n][^i][^c][^k]name)$/gi) && doneName != true) {
|
|
bName = $item.children('value:first').text();
|
|
doneName = true;
|
|
} else if ($(item).attr('var').match(/^(ctry|country.*)$/gi) && doneCountry != true) {
|
|
bCountry = $item.children('value:first').text();
|
|
doneCountry = true;
|
|
}
|
|
});
|
|
|
|
var bXID = $(this).find('field[var="jid"] value:first').text();
|
|
var dName = bName;
|
|
|
|
// Override "undefined" value
|
|
if(!bXID)
|
|
bXID = '';
|
|
if(!bName)
|
|
bName = _e("Unknown name");
|
|
if(!bCountry)
|
|
bCountry = _e("Unknown country");
|
|
|
|
// User hash
|
|
var bHash = hex_md5(bXID);
|
|
|
|
// HTML code
|
|
var bHTML = '<div class="oneresult ' + target + '-oneresult ' + bHash + '">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
'<div class="one-fn">' + bName + '</div>' +
|
|
'<div class="one-ctry">' + bCountry + '</div>' +
|
|
'<div class="one-jid">' + bXID + '</div>' +
|
|
'<div class="buttons-container">';
|
|
|
|
// The buddy is not in our buddy list?
|
|
if(!exists('#buddy-list .buddy[data-xid="' + escape(bXID) + '"]'))
|
|
bHTML += '<a href="#" class="one-add one-vjud one-button talk-images">' + _e("Add") + '</a>';
|
|
|
|
// Chat button, if not in welcome/directory mode
|
|
if(target == 'discovery')
|
|
bHTML += '<a href="#" class="one-chat one-vjud one-button talk-images">' + _e("Chat") + '</a>';
|
|
|
|
// Profile button, if not in discovery mode
|
|
else
|
|
bHTML += '<a href="#" class="one-profile one-vjud one-button talk-images">' + _e("Profile") + '</a>';
|
|
|
|
// Close the HTML element
|
|
bHTML += '</div></div>';
|
|
|
|
$(bPath).append(bHTML);
|
|
|
|
// Click events
|
|
$(bPath + ' .' + bHash + ' a').click(function() {
|
|
// Buddy add
|
|
if($(this).is('.one-add')) {
|
|
$(this).hide();
|
|
|
|
addThisContact(bXID, dName);
|
|
}
|
|
|
|
// Buddy chat
|
|
if($(this).is('.one-chat')) {
|
|
if(target == 'discovery')
|
|
closeDiscovery();
|
|
|
|
checkChatCreate(bXID, 'chat', '', '', dName);
|
|
}
|
|
|
|
// Buddy profile
|
|
if($(this).is('.one-profile'))
|
|
openUserInfos(bXID);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Get the user's avatar
|
|
if(bXID)
|
|
getAvatar(bXID, 'cache', 'true', 'forget');
|
|
});
|
|
|
|
// No result?
|
|
if(!$(handleXML).find('item').size())
|
|
noResultDataForm(pathID);
|
|
|
|
// Previous button
|
|
buttonsDataForm(type, 'back', sessionID, from, bNode, bSession, target, pathID);
|
|
}
|
|
|
|
// Command to complete
|
|
else if(xElement.attr('xmlns') || ((type == 'subscribe') && xRegister)) {
|
|
// We display the elements
|
|
fillDataForm(handleXML, sessionID);
|
|
|
|
// We display the buttons
|
|
if(bStatus != 'completed')
|
|
buttonsDataForm(type, 'submit', sessionID, from, bNode, bSession, target, pathID);
|
|
else
|
|
buttonsDataForm(type, 'back', sessionID, from, bNode, bSession, target, pathID);
|
|
}
|
|
|
|
// Command completed or subscription done
|
|
else if(((bStatus == 'completed') && (type == 'command')) || (!xRegister && (type == 'subscribe'))) {
|
|
// Display the good text
|
|
var cNote = $(xCommand).find('note');
|
|
|
|
// Any note?
|
|
if(cNote.size()) {
|
|
cNote.each(function() {
|
|
$(pathID).append(
|
|
'<div class="onetitle ' + target + '-oneresult">' + $(this).text().htmlEnc() + '</div>'
|
|
);
|
|
});
|
|
}
|
|
|
|
// Default text
|
|
else
|
|
$(pathID).append('<div class="oneinstructions ' + target + '-oneresult">' + _e("Your form has been sent.") + '</div>');
|
|
|
|
// Display the back button
|
|
buttonsDataForm(type, 'back', sessionID, from, '', '', target, pathID);
|
|
|
|
// Add the gateway to our roster if subscribed
|
|
if(type == 'subscribe')
|
|
addThisContact(from);
|
|
}
|
|
|
|
// Command canceled
|
|
else if((bStatus == 'canceled') && (type == 'command')) {
|
|
if(target == 'discovery')
|
|
startDiscovery();
|
|
else if(target == 'adhoc')
|
|
dataForm(from, 'command', '', '', 'adhoc');
|
|
}
|
|
|
|
// No items for this query
|
|
else
|
|
noResultDataForm(pathID);
|
|
}
|
|
|
|
else if(type == 'command') {
|
|
if($(handleXML).find('item').attr('jid')) {
|
|
// We display the elements
|
|
$(handleXML).find('item').each(function() {
|
|
// We parse the received xml
|
|
var itemHost = $(this).attr('jid');
|
|
var itemNode = $(this).attr('node');
|
|
var itemName = $(this).attr('name');
|
|
var itemHash = hex_md5(itemHost);
|
|
|
|
// We display the waiting element
|
|
$(pathID).prepend(
|
|
'<div class="oneresult ' + target + '-oneresult ' + itemHash + '" onclick="return dataForm(\'' + encodeOnclick(itemHost) + '\', \'command\', \'' + encodeOnclick(itemNode) + '\', \'execute\', \'' + encodeOnclick(target) + '\');">' +
|
|
'<div class="one-name">' + itemName + '</div>' +
|
|
'<div class="one-next">»</div>' +
|
|
'</div>'
|
|
);
|
|
});
|
|
}
|
|
|
|
// Else, there are no items for this query
|
|
else
|
|
noResultDataForm(pathID);
|
|
}
|
|
}
|
|
|
|
// Focus on the first input
|
|
$(document).oneTime(10, function() {
|
|
$(pathID + ' input:visible:first').focus();
|
|
});
|
|
|
|
// Hide the wait icon
|
|
$('#' + target + ' .wait').hide();
|
|
}
|
|
|
|
// Fills the dataform elements
|
|
function fillDataForm(xml, id) {
|
|
/* REF: http://xmpp.org/extensions/xep-0004.html */
|
|
|
|
// Initialize new vars
|
|
var target = id.split('-')[0];
|
|
var pathID = '#' + target + ' .results[data-session="' + id + '"]';
|
|
var selector, is_dataform;
|
|
|
|
// Is it a dataform?
|
|
if($(xml).find('x[xmlns="' + NS_XDATA + '"]').size())
|
|
is_dataform = true;
|
|
else
|
|
is_dataform = false;
|
|
|
|
// Determines the good selector to use
|
|
if(is_dataform)
|
|
selector = $(xml).find('x[xmlns="' + NS_XDATA + '"]');
|
|
else
|
|
selector = $(xml);
|
|
|
|
// Form title
|
|
selector.find('title').each(function() {
|
|
$(pathID).append(
|
|
'<div class="onetitle ' + target + '-oneresult">' + $(this).text().htmlEnc() + '</div>'
|
|
);
|
|
});
|
|
|
|
// Form instructions
|
|
selector.find('instructions').each(function() {
|
|
$(pathID).append(
|
|
'<div class="oneinstructions ' + target + '-oneresult">' + $(this).text().htmlEnc() + '</div>'
|
|
);
|
|
});
|
|
|
|
// Register?
|
|
if(!is_dataform) {
|
|
// Items to detect
|
|
var reg_names = [_e("Nickname"), _e("Name"), _e("Password"), _e("E-mail")];
|
|
var reg_ids = ['username', 'name', 'password', 'email'];
|
|
|
|
// Append these inputs
|
|
for(a in reg_names) {
|
|
selector.find(reg_ids[a]).each(function() {
|
|
$(pathID).append(
|
|
'<div class="oneresult ' + target + '-oneresult">' +
|
|
'<label>' + reg_names[a] + '</label>' +
|
|
'<input name="' + reg_ids[a] + '" type="text" class="register-special dataform-i" />' +
|
|
'</div>'
|
|
);
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Dataform?
|
|
selector.find('field').each(function() {
|
|
// We parse the received xml
|
|
var type = $(this).attr('type');
|
|
var label = $(this).attr('label');
|
|
var field = $(this).attr('var');
|
|
var value = $(this).find('value:first').text();
|
|
var required = '';
|
|
|
|
// No value?
|
|
if(!field)
|
|
return;
|
|
|
|
// Required input?
|
|
if($(this).find('required').size())
|
|
required = ' required=""';
|
|
|
|
// Compatibility fix
|
|
if(!label)
|
|
label = field;
|
|
|
|
if(!type)
|
|
type = '';
|
|
|
|
// Generate some values
|
|
var input;
|
|
var hideThis = '';
|
|
|
|
// Fixed field
|
|
if(type == 'fixed')
|
|
$(pathID).append('<div class="oneinstructions">' + value.htmlEnc() + '</div>');
|
|
|
|
else {
|
|
// Hidden field
|
|
if(type == 'hidden') {
|
|
hideThis = ' style="display: none;"';
|
|
input = '<input name="' + encodeQuotes(field) + '" data-type="' + encodeQuotes(type) + '" type="hidden" class="dataform-i" value="' + encodeQuotes(value) + '" ' + required + ' />';
|
|
}
|
|
|
|
// Boolean field
|
|
else if(type == 'boolean') {
|
|
var checked;
|
|
|
|
if(value == '1')
|
|
checked = 'checked';
|
|
else
|
|
checked = '';
|
|
|
|
input = '<input name="' + encodeQuotes(field) + '" type="checkbox" data-type="' + encodeQuotes(type) + '" class="dataform-i df-checkbox" ' + checked + required + ' />';
|
|
}
|
|
|
|
// List-single/list-multi field
|
|
else if((type == 'list-single') || (type == 'list-multi')) {
|
|
var multiple = '';
|
|
|
|
// Multiple options?
|
|
if(type == 'list-multi')
|
|
multiple = ' multiple=""';
|
|
|
|
// Append the select field
|
|
input = '<select name="' + encodeQuotes(field) + '" data-type="' + encodeQuotes(type) + '" class="dataform-i"' + required + multiple + '>';
|
|
var selected;
|
|
|
|
// Append the available options
|
|
$(this).find('option').each(function() {
|
|
var nLabel = $(this).attr('label');
|
|
var nValue = $(this).find('value').text();
|
|
|
|
// No label?
|
|
if(!nLabel)
|
|
nLabel = nValue;
|
|
|
|
// If this is the selected value
|
|
if(nValue == value)
|
|
selected = 'selected';
|
|
else
|
|
selected = '';
|
|
|
|
input += '<option ' + selected + ' value="' + encodeQuotes(nValue) + '">' + nLabel.htmlEnc() + '</option>';
|
|
});
|
|
|
|
input += '</select>';
|
|
}
|
|
|
|
// Text-multi field
|
|
else if(type == 'text-multi')
|
|
input = '<textarea rows="8" cols="60" data-type="' + encodeQuotes(type) + '" name="' + encodeQuotes(field) + '" class="dataform-i"' + required + '>' + value.htmlEnc() + '</textarea>';
|
|
|
|
// JID-multi field
|
|
else if(type == 'jid-multi') {
|
|
// Put the XID into an array
|
|
var xid_arr = [];
|
|
|
|
$(this).find('value').each(function() {
|
|
var cValue = $(this).text();
|
|
|
|
if(!existArrayValue(xid_arr, cValue))
|
|
xid_arr.push(cValue);
|
|
});
|
|
|
|
// Sort the array
|
|
xid_arr.sort();
|
|
|
|
// Create the input
|
|
var xid_value = '';
|
|
|
|
if(xid_arr.length) {
|
|
for(i in xid_arr) {
|
|
// Any pre-value
|
|
if(xid_value)
|
|
xid_value += ', ';
|
|
|
|
// Add the current XID
|
|
xid_value += xid_arr[i];
|
|
}
|
|
}
|
|
|
|
input = '<input name="' + encodeQuotes(field) + '" data-type="' + encodeQuotes(type) + '" type="text" class="dataform-i" value="' + encodeQuotes(xid_value) + '" placeholder="jack@jappix.com, jones@jappix.com"' + required + ' />';
|
|
}
|
|
|
|
// Other stuffs that are similar
|
|
else {
|
|
// Text-single field
|
|
var iType = 'text';
|
|
|
|
// Text-private field
|
|
if(type == 'text-private')
|
|
iType = 'password';
|
|
|
|
// JID-single field
|
|
else if(type == 'jid-single')
|
|
iType = 'email';
|
|
|
|
input = '<input name="' + encodeQuotes(field) + '" data-type="' + encodeQuotes(type) + '" type="' + iType + '" class="dataform-i" value="' + encodeQuotes(value) + '"' + required + ' />';
|
|
}
|
|
|
|
// Append the HTML markup for this field
|
|
$(pathID).append(
|
|
'<div class="oneresult ' + target + '-oneresult"' + hideThis + '>' +
|
|
'<label>' + label.htmlEnc() + '</label>' +
|
|
input +
|
|
'</div>'
|
|
);
|
|
}
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the dataform type
|
|
function getDataFormType(host, node, id) {
|
|
var iq = new JSJaCIQ();
|
|
iq.setID(id + '-' + genID());
|
|
iq.setTo(host);
|
|
iq.setType('get');
|
|
|
|
var iqQuery = iq.setQuery(NS_DISCO_INFO);
|
|
|
|
if(node)
|
|
iqQuery.setAttribute('node', node);
|
|
|
|
con.send(iq, handleThisBrowse);
|
|
}
|
|
|
|
// Handles the browse stanza
|
|
function handleThisBrowse(iq) {
|
|
/* REF: http://xmpp.org/registrar/disco-categories.html */
|
|
|
|
var id = iq.getID();
|
|
var splitted = id.split('-');
|
|
var target = splitted[0];
|
|
var sessionID = target + '-' + splitted[1];
|
|
var from = fullXID(getStanzaFrom(iq));
|
|
var hash = hex_md5(from);
|
|
var handleXML = iq.getQuery();
|
|
var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]';
|
|
|
|
// We first remove the waiting element
|
|
$(pathID + ' .disco-wait .' + hash).remove();
|
|
|
|
if($(handleXML).find('identity').attr('type')) {
|
|
var category = $(handleXML).find('identity').attr('category');
|
|
var type = $(handleXML).find('identity').attr('type');
|
|
var named = $(handleXML).find('identity').attr('name');
|
|
|
|
if(named)
|
|
gName = named;
|
|
else
|
|
gName = '';
|
|
|
|
var one, two, three, four, five;
|
|
|
|
// Get the features that this entity supports
|
|
var findFeature = $(handleXML).find('feature');
|
|
|
|
for(i in findFeature) {
|
|
var current = findFeature.eq(i).attr('var');
|
|
|
|
switch(current) {
|
|
case NS_SEARCH:
|
|
one = 1;
|
|
break;
|
|
|
|
case NS_MUC:
|
|
two = 1;
|
|
break;
|
|
|
|
case NS_REGISTER:
|
|
three = 1;
|
|
break;
|
|
|
|
case NS_COMMANDS:
|
|
four = 1;
|
|
break;
|
|
|
|
case NS_DISCO_ITEMS:
|
|
five = 1;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
var buttons = Array(one, two, three, four, five);
|
|
|
|
// We define the toolbox links depending on the supported features
|
|
var tools = '';
|
|
var aTools = Array('search', 'join', 'subscribe', 'command', 'browse');
|
|
var bTools = Array(_e("Search"), _e("Join"), _e("Subscribe"), _e("Command"), _e("Browse"));
|
|
|
|
for(i in buttons) {
|
|
if(buttons[i])
|
|
tools += '<a href="#" class="one-button ' + aTools[i] + ' talk-images" onclick="return dataForm(\'' + encodeOnclick(from) + '\', \'' + encodeOnclick(aTools[i]) + '\', \'\', \'\', \'' + encodeOnclick(target) + '\');" title="' + encodeOnclick(bTools[i]) + '"></a>';
|
|
}
|
|
|
|
// As defined in the ref, we detect the type of each category to put an icon
|
|
switch(category) {
|
|
case 'account':
|
|
case 'auth':
|
|
case 'automation':
|
|
case 'client':
|
|
case 'collaboration':
|
|
case 'component':
|
|
case 'conference':
|
|
case 'directory':
|
|
case 'gateway':
|
|
case 'headline':
|
|
case 'hierarchy':
|
|
case 'proxy':
|
|
case 'pubsub':
|
|
case 'server':
|
|
case 'store':
|
|
break;
|
|
|
|
default:
|
|
category = 'others';
|
|
}
|
|
|
|
// We display the item we found
|
|
$(pathID + ' .disco-' + category + ' .disco-category-title').after(
|
|
'<div class="oneresult ' + target + '-oneresult ' + hash + '">' +
|
|
'<div class="one-icon ' + category + ' talk-images"></div>' +
|
|
'<div class="one-host">' + from + '</div>' +
|
|
'<div class="one-type">' + gName + '</div>' +
|
|
'<div class="one-actions">' + tools + '</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// We display the category
|
|
$(pathID + ' .disco-' + category).show();
|
|
}
|
|
|
|
else {
|
|
$(pathID + ' .disco-others .disco-category-title').after(
|
|
'<div class="oneresult ' + target + '-oneresult">' +
|
|
'<div class="one-icon down talk-images"></div>' +
|
|
'<div class="one-host">' + from + '</div>' +
|
|
'<div class="one-type">' + _e("Service offline or broken") + '</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// We display the category
|
|
$(pathID + ' .disco-others').show();
|
|
}
|
|
|
|
// We hide the waiting stuffs if there's no remaining loading items
|
|
if(!$(pathID + ' .disco-wait .' + target + '-oneresult').size())
|
|
$(pathID + ' .disco-wait, #' + target + ' .wait').hide();
|
|
}
|
|
|
|
// Cleans the current data-form popup
|
|
function cleanDataForm(target) {
|
|
if(target == 'discovery')
|
|
cleanDiscovery();
|
|
else
|
|
$('#' + target + ' div.results').empty();
|
|
}
|
|
|
|
// Displays the no result indicator
|
|
function noResultDataForm(path) {
|
|
$(path).prepend('<p class="no-results">' + _e("Sorry, but the entity didn't return any result!") + '</p>');
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the discovery JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 03/03/11
|
|
|
|
*/
|
|
|
|
// Opens the discovery popup
|
|
function openDiscovery() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Service discovery") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="discovery-head">' +
|
|
'<div class="disco-server-text">' + _e("Server to query") + '</div>' +
|
|
|
|
'<input name="disco-server-input" class="disco-server-input" value="' + encodeQuotes(HOST_MAIN) + '" />' +
|
|
'</div>' +
|
|
|
|
'<div class="results discovery-results">' +
|
|
'<div class="disco-category disco-account">' +
|
|
'<p class="disco-category-title">' + _e("Accounts") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-auth">' +
|
|
'<p class="disco-category-title">' + _e("Authentications") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-automation">' +
|
|
'<p class="disco-category-title">' + _e("Automation") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-client">' +
|
|
'<p class="disco-category-title">' + _e("Clients") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-collaboration">' +
|
|
'<p class="disco-category-title">' + _e("Collaboration") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-component">' +
|
|
'<p class="disco-category-title">' + _e("Components") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-conference">' +
|
|
'<p class="disco-category-title">' + _e("Rooms") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-directory">' +
|
|
'<p class="disco-category-title">' + _e("Directories") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-gateway">' +
|
|
'<p class="disco-category-title">' + _e("Gateways") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-headline">' +
|
|
'<p class="disco-category-title">' + _e("News") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-hierarchy">' +
|
|
'<p class="disco-category-title">' + _e("Hierarchy") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-proxy">' +
|
|
'<p class="disco-category-title">' + _e("Proxies") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-pubsub">' +
|
|
'<p class="disco-category-title">' + _e("Publication/Subscription") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-server">' +
|
|
'<p class="disco-category-title">' + _e("Server") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-store">' +
|
|
'<p class="disco-category-title">' + _e("Storage") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-others">' +
|
|
'<p class="disco-category-title">' + _e("Others") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="disco-category disco-wait">' +
|
|
'<p class="disco-category-title">' + _e("Loading") + '</p>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('discovery', html);
|
|
|
|
// Associate the events
|
|
launchDiscovery();
|
|
|
|
// We request a disco to the default server
|
|
startDiscovery();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Quits the discovery popup
|
|
function closeDiscovery() {
|
|
// Destroy the popup
|
|
destroyPopup('discovery');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Launches a discovery
|
|
function startDiscovery() {
|
|
/* REF: http://xmpp.org/extensions/xep-0030.html */
|
|
|
|
// We get the server to query
|
|
var discoServer = $('#discovery .disco-server-input').val();
|
|
|
|
// We launch the items query
|
|
dataForm(discoServer, 'browse', '', '', 'discovery');
|
|
|
|
logThis('Service discovery launched: ' + discoServer);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Cleans the discovery results
|
|
function cleanDiscovery() {
|
|
// We remove the results
|
|
$('#discovery .discovery-oneresult, #discovery .oneinstructions, #discovery .onetitle, #discovery .no-results').remove();
|
|
|
|
// We clean the user info
|
|
$('#discovery .disco-server-info').text('');
|
|
|
|
// We hide the wait icon, the no result alert and the results
|
|
$('#discovery .wait, #discovery .disco-category').hide();
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchDiscovery() {
|
|
// Click event
|
|
$('#discovery .bottom .finish').click(closeDiscovery);
|
|
|
|
// Keyboard event
|
|
$('#discovery .disco-server-input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// No value?
|
|
if(!$(this).val())
|
|
$(this).val(HOST_MAIN);
|
|
|
|
// Start the discovery
|
|
startDiscovery();
|
|
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the directory JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 03/03/11
|
|
|
|
*/
|
|
|
|
// Opens the directory popup
|
|
function openDirectory() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("User directory") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="directory-head">' +
|
|
'<div class="directory-server-text">' + _e("Server to query") + '</div>' +
|
|
|
|
'<input name="directory-server-input" class="directory-server-input" value="' + encodeQuotes(HOST_VJUD) + '" />' +
|
|
'</div>' +
|
|
|
|
'<div class="results directory-results"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('directory', html);
|
|
|
|
// Associate the events
|
|
launchDirectory();
|
|
|
|
// Start a search!
|
|
startDirectory();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Quits the directory popup
|
|
function closeDirectory() {
|
|
// Destroy the popup
|
|
destroyPopup('directory');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Launches a directory search
|
|
function startDirectory() {
|
|
// Get the server to query
|
|
var server = $('#directory .directory-server-input').val();
|
|
|
|
// Launch the search!
|
|
dataForm($('#directory .directory-server-input').val(), 'search', '', '', 'directory');
|
|
|
|
logThis('Directory search launched: ' + server);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchDirectory() {
|
|
// Click event
|
|
$('#directory .bottom .finish').click(closeDirectory);
|
|
|
|
// Keyboard event
|
|
$('#directory .directory-server-input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// No value?
|
|
if(!$(this).val())
|
|
$(this).val(HOST_VJUD);
|
|
|
|
// Start the directory search
|
|
startDirectory();
|
|
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the Ad-Hoc JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 11/07/11
|
|
|
|
*/
|
|
|
|
// Opens the adhoc popup
|
|
function openAdHoc() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Commands") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="adhoc-head"></div>' +
|
|
|
|
'<div class="results adhoc-results"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('adhoc', html);
|
|
|
|
// Associate the events
|
|
launchAdHoc();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Quits the adhoc popup
|
|
function closeAdHoc() {
|
|
// Destroy the popup
|
|
destroyPopup('adhoc');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Retrieves an entity adhoc command
|
|
function retrieveAdHoc(xid) {
|
|
// Open the popup
|
|
openAdHoc();
|
|
|
|
// Add a XID marker
|
|
$('#adhoc .adhoc-head').html('<b>' + getBuddyName(xid).htmlEnc() + '</b> (' + xid.htmlEnc() + ')');
|
|
|
|
// Get the highest entity resource
|
|
var highest = highestPriority(xid);
|
|
|
|
if(highest)
|
|
xid = highest;
|
|
|
|
// Start a new adhoc command
|
|
dataForm(xid, 'command', '', '', 'adhoc');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Starts an adhoc command on the user server
|
|
function serverAdHoc(server) {
|
|
// Open the popup
|
|
openAdHoc();
|
|
|
|
// Add a XID marker
|
|
$('#adhoc .adhoc-head').html('<b>' + server.htmlEnc() + '</b>');
|
|
|
|
// Start a new adhoc command
|
|
dataForm(server, 'command', '', '', 'adhoc');
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchAdHoc() {
|
|
// Click event
|
|
$('#adhoc .bottom .finish').click(closeAdHoc);
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the privacy JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 20/07/13
|
|
|
|
*/
|
|
|
|
// Opens the privacy popup
|
|
function openPrivacy() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Privacy") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="privacy-head">' +
|
|
'<div class="list-left">' +
|
|
'<span>' + _e("Choose") + '</span>' +
|
|
'<select disabled=""></select>' +
|
|
'<a href="#" class="list-remove one-button talk-images" title="' + _e("Remove") + '"></a>' +
|
|
'</div>' +
|
|
|
|
'<div class="list-center"></div>' +
|
|
|
|
'<div class="list-right">' +
|
|
'<span>' + _e("Add") + '</span>' +
|
|
'<input type="text" placeholder="' + _e("List name") + '" />' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="privacy-item">' +
|
|
'<span>' + _e("Item") + '</span>' +
|
|
'<select disabled=""></select>' +
|
|
'<a href="#" class="item-add one-button talk-images" title="' + _e("Add") + '"></a>' +
|
|
'<a href="#" class="item-remove one-button talk-images" title="' + _e("Remove") + '"></a>' +
|
|
'<a href="#" class="item-save one-button talk-images">' + _e("Save") + '</a>' +
|
|
|
|
'<div class="clear"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="privacy-form">' +
|
|
'<div class="privacy-first">' +
|
|
'<label><input type="radio" name="action" value="allow" disabled="" />' + _e("Allow") + '</label>' +
|
|
'<label><input type="radio" name="action" value="deny" disabled="" />' + _e("Deny") + '</label>' +
|
|
'</div>' +
|
|
|
|
'<div class="privacy-second">' +
|
|
'<label><input type="radio" name="type" value="jid" disabled="" />' + _e("Address") + '</label>' +
|
|
'<input type="text" name="jid" disabled="" />' +
|
|
|
|
'<label><input type="radio" name="type" value="group" disabled="" />' + _e("Group") + '</label>' +
|
|
'<select name="group" disabled="">' + groupsToHtmlPrivacy() + '</select>' +
|
|
|
|
'<label><input type="radio" name="type" value="subscription" disabled="" />' + _e("Subscription") + '</label>' +
|
|
'<select name="subscription" disabled="">' +
|
|
'<option value="none">' + _e("None") + '</option>' +
|
|
'<option value="both">' + _e("Both") + '</option>' +
|
|
'<option value="from">' + _e("From") + '</option>' +
|
|
'<option value="to">' + _e("To") + '</option>' +
|
|
'</select>' +
|
|
|
|
'<label><input type="radio" name="type" value="everybody" disabled="" />' + _e("Everybody") + '</label>' +
|
|
'</div>' +
|
|
|
|
'<div class="privacy-third">' +
|
|
'<label><input type="checkbox" name="send-messages" disabled="" />' + _e("Send messages") + '</label>' +
|
|
'<label><input type="checkbox" name="send-queries" disabled="" />' + _e("Send queries") + '</label>' +
|
|
'<label><input type="checkbox" name="see-status" disabled="" />' + _e("See my status") + '</label>' +
|
|
'<label><input type="checkbox" name="send-status" disabled="" />' + _e("Send his/her status") + '</label>' +
|
|
'<label><input type="checkbox" name="everything" disabled="" />' + _e("Everything") + '</label>' +
|
|
'</div>' +
|
|
|
|
'<div class="clear"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="privacy-active">' +
|
|
'<label>' + _e("Order") + '<input type="text" name="order" value="1" disabled="" /></label>' +
|
|
|
|
'<div class="privacy-active-elements">' +
|
|
'<label><input type="checkbox" name="active" disabled="" />' + _e("Active for this session") + '</label>' +
|
|
'<label><input type="checkbox" name="default" disabled="" />' + _e("Always active") + '</label>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('privacy', html);
|
|
|
|
// Associate the events
|
|
launchPrivacy();
|
|
|
|
// Display the available privacy lists
|
|
displayListsPrivacy();
|
|
|
|
// Get the first list items
|
|
displayItemsPrivacy();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Quits the privacy popup
|
|
function closePrivacy() {
|
|
// Destroy the popup
|
|
destroyPopup('privacy');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Sets the received state for privacy block list
|
|
function receivedPrivacy() {
|
|
// Store marker
|
|
setDB('privacy-marker', 'available', 'true');
|
|
|
|
// Show privacy elements
|
|
$('.privacy-hidable').show();
|
|
}
|
|
|
|
// Gets available privacy lists
|
|
function listPrivacy() {
|
|
// Build query
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
iq.setQuery(NS_PRIVACY);
|
|
|
|
con.send(iq, handleListPrivacy);
|
|
|
|
logThis('Getting available privacy list(s)...');
|
|
}
|
|
|
|
// Handles available privacy lists
|
|
function handleListPrivacy(iq) {
|
|
// Error?
|
|
if(iq.getType() == 'error')
|
|
return logThis('Privacy lists not supported!', 2);
|
|
|
|
// Get IQ query content
|
|
var iqQuery = iq.getQuery();
|
|
|
|
// Save the content
|
|
setDB('privacy-lists', 'available', xmlToString(iqQuery));
|
|
|
|
// Any block list?
|
|
if($(iqQuery).find('list[name="block"]').size()) {
|
|
// Not the default one?
|
|
if(!$(iqQuery).find('default[name="block"]').size())
|
|
changePrivacy('block', 'default');
|
|
else
|
|
setDB('privacy-marker', 'default', 'block');
|
|
|
|
// Not the active one?
|
|
if(!$(iqQuery).find('active[name="block"]').size())
|
|
changePrivacy('block', 'active');
|
|
else
|
|
setDB('privacy-marker', 'active', 'block');
|
|
|
|
// Get the block list rules
|
|
getPrivacy('block');
|
|
}
|
|
|
|
// Apply the received marker here
|
|
else
|
|
receivedPrivacy();
|
|
|
|
logThis('Got available privacy list(s).', 3);
|
|
}
|
|
|
|
// Gets privacy lists
|
|
function getPrivacy(list) {
|
|
// Build query
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
// Privacy query
|
|
var iqQuery = iq.setQuery(NS_PRIVACY);
|
|
iqQuery.appendChild(iq.buildNode('list', {'xmlns': NS_PRIVACY, 'name': list}));
|
|
|
|
con.send(iq, handleGetPrivacy);
|
|
|
|
// Must show the wait item?
|
|
if(exists('#privacy'))
|
|
$('#privacy .wait').show();
|
|
|
|
logThis('Getting privacy list(s): ' + list);
|
|
}
|
|
|
|
// Handles privacy lists
|
|
function handleGetPrivacy(iq) {
|
|
// Apply a "received" marker
|
|
receivedPrivacy();
|
|
|
|
// Store the data for each list
|
|
$(iq.getQuery()).find('list').each(function() {
|
|
// Read list name
|
|
var list_name = $(this).attr('name');
|
|
|
|
// Store list content
|
|
setDB('privacy', list_name, xmlToString(this));
|
|
|
|
// Is this a block list?
|
|
if(list_name == 'block') {
|
|
// Reset buddies
|
|
$('#buddy-list .buddy').removeClass('blocked');
|
|
|
|
// XID types
|
|
$(this).find('item[action="deny"][type="jid"]').each(function() {
|
|
$('#buddy-list .buddy[data-xid="' + escape($(this).attr('value')) + '"]').addClass('blocked');
|
|
});
|
|
|
|
// Group types
|
|
$(this).find('item[action="deny"][type="group"]').each(function() {
|
|
$('#buddy-list .group' + hex_md5($(this).attr('value')) + ' .buddy').addClass('blocked');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Must display it to the popup?
|
|
if(exists('#privacy')) {
|
|
displayItemsPrivacy();
|
|
|
|
$('#privacy .wait').hide();
|
|
}
|
|
|
|
logThis('Got privacy list(s).', 3);
|
|
}
|
|
|
|
// Sets a privacy list
|
|
function setPrivacy(list, types, values, actions, orders, presence_in, presence_out, msg, iq_p) {
|
|
// Build query
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// Privacy query
|
|
var iqQuery = iq.setQuery(NS_PRIVACY);
|
|
var iqList = iqQuery.appendChild(iq.buildNode('list', {'xmlns': NS_PRIVACY, 'name': list}));
|
|
|
|
// Build the item elements
|
|
if(types && types.length) {
|
|
for(var i = 0; i < types.length; i++) {
|
|
// Item element
|
|
var iqItem = iqList.appendChild(iq.buildNode('item', {'xmlns': NS_PRIVACY}));
|
|
|
|
// Item attributes
|
|
if(types[i])
|
|
iqItem.setAttribute('type', types[i]);
|
|
if(values[i])
|
|
iqItem.setAttribute('value', values[i]);
|
|
if(actions[i])
|
|
iqItem.setAttribute('action', actions[i]);
|
|
if(orders[i])
|
|
iqItem.setAttribute('order', orders[i]);
|
|
|
|
// Child elements
|
|
if(presence_in[i])
|
|
iqItem.appendChild(iq.buildNode('presence-in', {'xmlns': NS_PRIVACY}));
|
|
if(presence_out[i])
|
|
iqItem.appendChild(iq.buildNode('presence-out', {'xmlns': NS_PRIVACY}));
|
|
if(msg[i])
|
|
iqItem.appendChild(iq.buildNode('message', {'xmlns': NS_PRIVACY}));
|
|
if(iq_p[i])
|
|
iqItem.appendChild(iq.buildNode('iq', {'xmlns': NS_PRIVACY}));
|
|
}
|
|
}
|
|
|
|
con.send(iq, function(iq) {
|
|
if(iq.getType() == 'result')
|
|
logThis('Sent privacy list.');
|
|
else
|
|
logThis('Error sending privacy list.', 1);
|
|
});
|
|
|
|
logThis('Sending privacy list: ' + list);
|
|
}
|
|
|
|
// Push a privacy list item to a list
|
|
function pushPrivacy(list, type, value, action, presence_in, presence_out, msg, iq_p, hash, special_action) {
|
|
// Read the stored elements (to add them)
|
|
var stored = XMLFromString(getDB('privacy', list));
|
|
|
|
// Read the first value
|
|
var first_val = value[0];
|
|
|
|
// Order generation flow
|
|
var order = [];
|
|
var highest_order = 0;
|
|
|
|
// Must remove the given value?
|
|
if(special_action == 'remove') {
|
|
type = [];
|
|
value = [];
|
|
action = [];
|
|
presence_in = [];
|
|
presence_out = [];
|
|
iq_p = [];
|
|
}
|
|
|
|
// Serialize them to an array
|
|
$(stored).find('item').each(function() {
|
|
// Attributes
|
|
var c_type = $(this).attr('type');
|
|
var c_value = $(this).attr('value');
|
|
var c_action = $(this).attr('action');
|
|
var c_order = $(this).attr('order');
|
|
|
|
// Generate hash
|
|
var c_hash = hex_md5(c_type + c_value);
|
|
|
|
// Do not push it twice!
|
|
if(((c_hash != hash) && (special_action != 'roster')) || ((first_val != c_value) && (special_action == 'roster'))) {
|
|
if(!c_type)
|
|
c_type = '';
|
|
if(!c_value)
|
|
c_value = '';
|
|
if(!c_action)
|
|
c_action = '';
|
|
if(!c_order)
|
|
c_order = '';
|
|
|
|
if(!isNaN(c_order) && parseInt(c_order) > highest_order)
|
|
highest_order = parseInt(c_order);
|
|
|
|
type.push(c_type);
|
|
value.push(c_value);
|
|
action.push(c_action);
|
|
order.push(c_order);
|
|
|
|
// Child elements
|
|
if($(this).find('presence-in').size())
|
|
presence_in.push(true);
|
|
else
|
|
presence_in.push(false);
|
|
|
|
if($(this).find('presence-out').size())
|
|
presence_out.push(true);
|
|
else
|
|
presence_out.push(false);
|
|
|
|
if($(this).find('message').size())
|
|
msg.push(true);
|
|
else
|
|
msg.push(false);
|
|
|
|
if($(this).find('iq').size())
|
|
iq_p.push(true);
|
|
else
|
|
iq_p.push(false);
|
|
}
|
|
});
|
|
|
|
order.unshift((++highest_order) + '');
|
|
|
|
// Send it!
|
|
setPrivacy(list, type, value, action, order, presence_in, presence_out, msg, iq_p);
|
|
}
|
|
|
|
// Change a privacy list status
|
|
function changePrivacy(list, status) {
|
|
// Yet sent?
|
|
if(getDB('privacy-marker', status) == list)
|
|
return;
|
|
|
|
// Write a marker
|
|
setDB('privacy-marker', status, list);
|
|
|
|
// Build query
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// Privacy query
|
|
var iqQuery = iq.setQuery(NS_PRIVACY);
|
|
var iqStatus = iqQuery.appendChild(iq.buildNode(status, {'xmlns': NS_PRIVACY}));
|
|
|
|
// Can add a "name" attribute?
|
|
if(list)
|
|
iqStatus.setAttribute('name', list);
|
|
|
|
con.send(iq);
|
|
|
|
logThis('Changing privacy list status: ' + list + ' to: ' + status);
|
|
}
|
|
|
|
// Checks the privacy status (action) of a value
|
|
function statusPrivacy(list, value) {
|
|
return $(XMLFromString(getDB('privacy', list))).find('item[value="' + value + '"]').attr('action');
|
|
}
|
|
|
|
// Converts the groups array into a <option /> string
|
|
function groupsToHtmlPrivacy() {
|
|
var groups = getAllGroups();
|
|
var html = '';
|
|
|
|
// Generate HTML
|
|
for(i in groups) {
|
|
html += '<option value="' + encodeQuotes(groups[i]) +'">' + groups[i].htmlEnc() + '</option>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// Displays the privacy lists
|
|
function displayListsPrivacy() {
|
|
// Initialize
|
|
var code = '';
|
|
var select = $('#privacy .privacy-head .list-left select');
|
|
var data = XMLFromString(getDB('privacy-lists', 'available'));
|
|
|
|
// Parse the XML data!
|
|
$(data).find('list').each(function() {
|
|
var list_name = $(this).attr('name');
|
|
|
|
if(list_name)
|
|
code += '<option value="' + encodeQuotes(list_name) + '">' + list_name.htmlEnc() + '</option>';
|
|
});
|
|
|
|
// Apply HTML code
|
|
select.html(code);
|
|
|
|
// Not empty?
|
|
if(code)
|
|
select.removeAttr('disabled');
|
|
else
|
|
select.attr('disabled', true);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Displays the privacy items for a list
|
|
function displayItemsPrivacy() {
|
|
// Reset the form
|
|
clearFormPrivacy();
|
|
disableFormPrivacy();
|
|
|
|
// Initialize
|
|
var code = '';
|
|
var select = $('#privacy .privacy-item select');
|
|
var list = $('#privacy .privacy-head .list-left select').val();
|
|
|
|
// Reset the item select
|
|
select.html('');
|
|
|
|
// No list?
|
|
if(!list)
|
|
return false;
|
|
|
|
// Reset the list status
|
|
$('#privacy .privacy-active input[type="checkbox"]').removeAttr('checked');
|
|
|
|
// Display the list status
|
|
var status = ['active', 'default'];
|
|
|
|
for(s in status) {
|
|
if(getDB('privacy-marker', status[s]) == list)
|
|
$('#privacy .privacy-active input[name=' + status[s] + ']').attr('checked', true);
|
|
}
|
|
|
|
// Try to read the stored items
|
|
var items = XMLFromString(getDB('privacy', list));
|
|
|
|
// Must retrieve the data?
|
|
if(!items) {
|
|
select.attr('disabled', true);
|
|
|
|
return getPrivacy(list);
|
|
}
|
|
|
|
else
|
|
select.removeAttr('disabled');
|
|
|
|
// Parse the XML data!
|
|
$(items).find('item').each(function() {
|
|
// Read attributes
|
|
var item_type = $(this).attr('type');
|
|
var item_value = $(this).attr('value');
|
|
var item_action = $(this).attr('action');
|
|
var item_order = $(this).attr('order');
|
|
|
|
// Generate hash
|
|
var item_hash = hex_md5(item_type + item_value + item_action + item_order);
|
|
|
|
// Read sub-elements
|
|
var item_presencein = $(this).find('presence-in').size();
|
|
var item_presenceout = $(this).find('presence-out').size();
|
|
var item_message = $(this).find('message').size();
|
|
var item_iq = $(this).find('iq').size();
|
|
|
|
// Apply default values (if missing)
|
|
if(!item_type)
|
|
item_type = '';
|
|
if(!item_value)
|
|
item_value = '';
|
|
if(!item_action)
|
|
item_action = 'allow';
|
|
if(!item_order)
|
|
item_order = '1';
|
|
|
|
// Apply sub-elements values
|
|
if(item_presencein)
|
|
item_presencein = 'true';
|
|
else
|
|
item_presencein = 'false';
|
|
|
|
if(item_presenceout)
|
|
item_presenceout = 'true';
|
|
else
|
|
item_presenceout = 'false';
|
|
|
|
if(item_message)
|
|
item_message = 'true';
|
|
else
|
|
item_message = 'false';
|
|
|
|
if(item_iq)
|
|
item_iq = 'true';
|
|
else
|
|
item_iq = 'false';
|
|
|
|
// Generate item description
|
|
var desc = '';
|
|
var desc_arr = [item_type, item_value, item_action, item_order];
|
|
|
|
for(d in desc_arr) {
|
|
// Nothing to display?
|
|
if(!desc_arr[d])
|
|
continue;
|
|
|
|
if(desc)
|
|
desc += ' - ';
|
|
|
|
desc += desc_arr[d];
|
|
}
|
|
|
|
// Add the select option
|
|
code += '<option data-type="' + encodeQuotes(item_type) + '" data-value="' + encodeQuotes(item_value) + '" data-action="' + encodeQuotes(item_action) + '" data-order="' + encodeQuotes(item_order) + '" data-presence_in="' + encodeQuotes(item_presencein) + '" data-presence_out="' + encodeQuotes(item_presenceout) + '" data-message="' + encodeQuotes(item_message) + '" data-iq="' + encodeQuotes(item_iq) + '" data-hash="' + encodeQuotes(item_hash) + '">' +
|
|
desc +
|
|
'</option>';
|
|
});
|
|
|
|
// Append the code
|
|
select.append(code);
|
|
|
|
// Display the first item form
|
|
var first_item = select.find('option:first');
|
|
displayFormPrivacy(
|
|
first_item.attr('data-type'),
|
|
first_item.attr('data-value'),
|
|
first_item.attr('data-action'),
|
|
first_item.attr('data-order'),
|
|
first_item.attr('data-presence_in'),
|
|
first_item.attr('data-presence_out'),
|
|
first_item.attr('data-message'),
|
|
first_item.attr('data-iq')
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Displays the privacy form for an item
|
|
function displayFormPrivacy(type, value, action, order, presence_in, presence_out, message, iq) {
|
|
// Reset the form
|
|
clearFormPrivacy();
|
|
|
|
// Apply the action
|
|
$('#privacy .privacy-first input[name="action"][value="' + action + '"]').attr('checked', true);
|
|
|
|
// Apply the type & value
|
|
var privacy_second = '#privacy .privacy-second';
|
|
var privacy_type = privacy_second + ' input[name="type"]';
|
|
var type_check, value_input;
|
|
|
|
switch(type) {
|
|
case 'jid':
|
|
type_check = privacy_type + '[value="jid"]';
|
|
value_input = privacy_second + ' input[type="text"][name="jid"]';
|
|
|
|
break;
|
|
|
|
case 'group':
|
|
type_check = privacy_type + '[value="group"]';
|
|
value_input = privacy_second + ' select[name="group"]';
|
|
|
|
break;
|
|
|
|
case 'subscription':
|
|
type_check = privacy_type + '[value="subscription"]';
|
|
value_input = privacy_second + ' select[name="subscription"]';
|
|
|
|
break;
|
|
|
|
default:
|
|
type_check = privacy_type + '[value="everybody"]';
|
|
|
|
break;
|
|
}
|
|
|
|
// Check the target
|
|
$(type_check).attr('checked', true);
|
|
|
|
// Can apply a value?
|
|
if(value_input)
|
|
$(value_input).val(value);
|
|
|
|
// Apply the things to do
|
|
var privacy_do = '#privacy .privacy-third input[type="checkbox"]';
|
|
|
|
if(presence_in == 'true')
|
|
$(privacy_do + '[name="send-status"]').attr('checked', true);
|
|
if(presence_out == 'true')
|
|
$(privacy_do + '[name="see-status"]').attr('checked', true);
|
|
if(message == 'true')
|
|
$(privacy_do + '[name="send-messages"]').attr('checked', true);
|
|
if(iq == 'true')
|
|
$(privacy_do + '[name="send-queries"]').attr('checked', true);
|
|
|
|
if(!$(privacy_do).filter(':checked').size())
|
|
$(privacy_do + '[name="everything"]').attr('checked', true);
|
|
|
|
// Apply the order
|
|
$('#privacy .privacy-active input[name="order"]').val(order);
|
|
|
|
// Enable the forms
|
|
$('#privacy .privacy-form input, #privacy .privacy-form select, #privacy .privacy-active input').removeAttr('disabled');
|
|
}
|
|
|
|
// Clears the privacy list form
|
|
function clearFormPrivacy() {
|
|
// Uncheck checkboxes & radio inputs
|
|
$('#privacy .privacy-form input[type="checkbox"], #privacy .privacy-form input[type="radio"]').removeAttr('checked');
|
|
|
|
// Reset select
|
|
$('#privacy .privacy-form select option').removeAttr('selected');
|
|
$('#privacy .privacy-form select option:first').attr('selected', true);
|
|
|
|
// Reset text input
|
|
$('#privacy .privacy-form input[type="text"]').val('');
|
|
|
|
// Reset order input
|
|
$('#privacy .privacy-active input[name="order"]').val('1');
|
|
}
|
|
|
|
// Disables the privacy list form
|
|
function disableFormPrivacy() {
|
|
$('#privacy .privacy-form input, #privacy .privacy-form select, #privacy .privacy-active input').attr('disabled', true);
|
|
}
|
|
|
|
// Enables the privacy list form
|
|
function enableFormPrivacy(rank) {
|
|
$('#privacy .privacy-' + rank + ' input, #privacy .privacy-' + rank + ' select').removeAttr('disabled');
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchPrivacy() {
|
|
// Click events
|
|
$('#privacy .bottom .finish').click(closePrivacy);
|
|
|
|
// Placeholder events
|
|
$('#privacy input[placeholder]').placeholder();
|
|
|
|
// Form events
|
|
$('#privacy .privacy-head a.list-remove').click(function() {
|
|
// Get list name
|
|
var list = $('#privacy .privacy-head .list-left select').val();
|
|
|
|
// No value?
|
|
if(!list)
|
|
return false;
|
|
|
|
// Remove it from popup
|
|
$('#privacy .privacy-head .list-left select option[value="' + list + '"]').remove();
|
|
|
|
// Nothing remaining?
|
|
if(!exists('#privacy .privacy-head .list-left select option'))
|
|
$('#privacy .privacy-head .list-left select option').attr('disabled', true);
|
|
|
|
// Empty the item select
|
|
$('#privacy .privacy-item select').attr('disabled', true).html('');
|
|
|
|
// Disable this list before removing it
|
|
var status = ['active', 'default'];
|
|
|
|
for(s in status) {
|
|
if(getDB('privacy-marker', status[s]) == list)
|
|
changePrivacy('', status[s]);
|
|
}
|
|
|
|
// Remove from server
|
|
setPrivacy(list);
|
|
|
|
// Reset the form
|
|
clearFormPrivacy();
|
|
disableFormPrivacy();
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#privacy .privacy-head .list-right input').keyup(function(e) {
|
|
// Not enter?
|
|
if(e.keyCode != 13)
|
|
return;
|
|
|
|
// Get list name
|
|
var list = $('#privacy .privacy-head .list-right input').val();
|
|
var select = '#privacy .privacy-head .list-left select';
|
|
var existed = true;
|
|
|
|
// Create the new element
|
|
if(!exists(select + ' option[value="' + list + '"]')) {
|
|
// Marker
|
|
existed = false;
|
|
|
|
// Create a new option
|
|
$(select).append('<option value="' + encodeQuotes(list) + '">' + list.htmlEnc() + '</option>');
|
|
|
|
// Reset the item select
|
|
$('#privacy .privacy-item select').attr('disabled', true).html('');
|
|
}
|
|
|
|
// Change the select value & enable it
|
|
$(select).val(list).removeAttr('disabled');
|
|
|
|
// Reset its value
|
|
$(this).val('');
|
|
|
|
// Reset the form
|
|
clearFormPrivacy();
|
|
disableFormPrivacy();
|
|
|
|
// Must reload the list items?
|
|
if(existed) {
|
|
displayItemsPrivacy();
|
|
$('#privacy .privacy-item select').removeAttr('disabled');
|
|
}
|
|
});
|
|
|
|
$('#privacy .privacy-head .list-left select').change(displayItemsPrivacy);
|
|
|
|
$('#privacy .privacy-item select').change(function() {
|
|
// Get the selected item
|
|
var item = $(this).find('option:selected');
|
|
|
|
// Display the data!
|
|
displayFormPrivacy(
|
|
item.attr('data-type'),
|
|
item.attr('data-value'),
|
|
item.attr('data-action'),
|
|
item.attr('data-order'),
|
|
item.attr('data-presence_in'),
|
|
item.attr('data-presence_out'),
|
|
item.attr('data-message'),
|
|
item.attr('data-iq')
|
|
);
|
|
});
|
|
|
|
$('#privacy .privacy-item a.item-add').click(function() {
|
|
// Cannot add anything?
|
|
if(!exists('#privacy .privacy-head .list-left select option:selected'))
|
|
return false;
|
|
|
|
// Disable item select
|
|
$('#privacy .privacy-item select').attr('disabled', true);
|
|
|
|
// Reset the form
|
|
clearFormPrivacy();
|
|
disableFormPrivacy();
|
|
|
|
// Enable first form item
|
|
enableFormPrivacy('first');
|
|
enableFormPrivacy('active');
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#privacy .privacy-item a.item-remove').click(function() {
|
|
// Cannot add anything?
|
|
if(!exists('#privacy .privacy-head .list-left select option:selected'))
|
|
return false;
|
|
|
|
// Get values
|
|
var list = $('#privacy .privacy-head .list-left select').val();
|
|
var selected = $('#privacy .privacy-item select option:selected');
|
|
var item = selected.attr('data-value');
|
|
var hash = selected.attr('data-hash');
|
|
|
|
// Remove it from popup
|
|
$('#privacy .privacy-item select option:selected').remove();
|
|
|
|
// No more items in this list?
|
|
if(!exists('#privacy .privacy-item select option')) {
|
|
// Disable this select
|
|
$('#privacy .privacy-item select').attr('disabled', true);
|
|
|
|
// Remove the privacy list select item
|
|
$('#privacy .privacy-head .list-left select option[value="' + list + '"]').remove();
|
|
|
|
// No more privacy lists?
|
|
if(!exists('#privacy .privacy-head .list-left select option'))
|
|
$('#privacy .privacy-head .list-left select').attr('disabled', true);
|
|
|
|
// Disable this list before removing it
|
|
var status = ['active', 'default'];
|
|
|
|
for(s in status) {
|
|
if(getDB('privacy-marker', status[s]) == list)
|
|
changePrivacy('', status[s]);
|
|
}
|
|
}
|
|
|
|
// Synchronize it with server
|
|
pushPrivacy(list, [], [item], [], [], [], [], [], [], hash, 'remove');
|
|
|
|
// Reset the form
|
|
clearFormPrivacy();
|
|
disableFormPrivacy();
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#privacy .privacy-item a.item-save').click(function() {
|
|
// Canot push item?
|
|
if(exists('#privacy .privacy-form input:disabled'))
|
|
return false;
|
|
|
|
// Get the hash
|
|
var item_hash = '';
|
|
|
|
if(!$('#privacy .privacy-item select').is(':disabled'))
|
|
item_hash = $('#privacy .privacy-item select option:selected').attr('data-hash');
|
|
|
|
// Read the form
|
|
var privacy_second = '#privacy .privacy-second';
|
|
var item_list = $('#privacy .privacy-head .list-left select').val();
|
|
var item_action = $('#privacy .privacy-first input[name="action"]').filter(':checked').val();
|
|
var item_type = $(privacy_second + ' input[name="type"]').filter(':checked').val();
|
|
var item_order = $('#privacy .privacy-active input[name="order"]').val();
|
|
var item_value = '';
|
|
|
|
// Switch the type to get the value
|
|
switch(item_type) {
|
|
case 'jid':
|
|
item_value = $(privacy_second + ' input[type="text"][name="jid"]').val();
|
|
|
|
break;
|
|
|
|
case 'group':
|
|
item_value = $(privacy_second + ' select[name="group"]').val();
|
|
|
|
break;
|
|
|
|
case 'subscription':
|
|
item_value = $(privacy_second + ' select[name="subscription"]').val();
|
|
|
|
break;
|
|
|
|
default:
|
|
item_type = '';
|
|
|
|
break;
|
|
}
|
|
|
|
// Get the selected things to do
|
|
var privacy_third_cb = '#privacy .privacy-third input[type="checkbox"]';
|
|
var item_prin = false;
|
|
var item_prout = false;
|
|
var item_msg = false;
|
|
var item_iq = false;
|
|
|
|
// Individual select?
|
|
if(!$(privacy_third_cb + '[name="everything"]').filter(':checked').size()) {
|
|
if($(privacy_third_cb + '[name="send-messages"]').filter(':checked').size())
|
|
item_msg = true;
|
|
if($(privacy_third_cb + '[name="send-queries"]').filter(':checked').size())
|
|
item_iq = true;
|
|
if($(privacy_third_cb + '[name="send-queries"]').filter(':checked').size())
|
|
item_iq = true;
|
|
if($(privacy_third_cb + '[name="see-status"]').filter(':checked').size())
|
|
item_prout = true;
|
|
if($(privacy_third_cb + '[name="send-status"]').filter(':checked').size())
|
|
item_prin = true;
|
|
}
|
|
|
|
// Push item to the server!
|
|
pushPrivacy(
|
|
item_list,
|
|
[item_type],
|
|
[item_value],
|
|
[item_action],
|
|
[item_order],
|
|
[item_prin],
|
|
[item_prout],
|
|
[item_msg],
|
|
[item_iq],
|
|
item_hash
|
|
);
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#privacy .privacy-first input').change(function() {
|
|
enableFormPrivacy('second');
|
|
});
|
|
|
|
$('#privacy .privacy-second input').change(function() {
|
|
enableFormPrivacy('third');
|
|
});
|
|
|
|
$('#privacy .privacy-third input[type="checkbox"]').change(function() {
|
|
// Target
|
|
var target = '#privacy .privacy-third input[type="checkbox"]';
|
|
|
|
// Must tick "everything" checkbox?
|
|
if(!$(target).filter(':checked').size())
|
|
$(target + '[name="everything"]').attr('checked', true);
|
|
|
|
// Must untick the other checkboxes?
|
|
else if($(this).is('[name="everything"]'))
|
|
$(target + ':not([name="everything"])').removeAttr('checked');
|
|
|
|
// Must untick "everything" checkbox?
|
|
else
|
|
$(target + '[name="everything"]').removeAttr('checked');
|
|
});
|
|
|
|
$('#privacy .privacy-active input[name="order"]').keyup(function() {
|
|
// Get the value
|
|
var value = $(this).val();
|
|
|
|
// No value?
|
|
if(!value)
|
|
return;
|
|
|
|
// Not a number?
|
|
if(isNaN(value))
|
|
value = 1;
|
|
else
|
|
value = parseInt(value);
|
|
|
|
// Negative?
|
|
if(value < 0)
|
|
value = value * -1;
|
|
|
|
// Apply the filtered value
|
|
$(this).val(value);
|
|
})
|
|
|
|
.blur(function() {
|
|
// No value?
|
|
if(!$(this).val())
|
|
$(this).val('1');
|
|
});
|
|
|
|
$('#privacy .privacy-active .privacy-active-elements input').change(function() {
|
|
// Get the values
|
|
var list_name = $('#privacy .privacy-head .list-left select').val();
|
|
var state_name = $(this).attr('name');
|
|
|
|
// Cannot continue?
|
|
if(!list_name || !state_name)
|
|
return;
|
|
|
|
// Change the current list status
|
|
if($(this).filter(':checked').size())
|
|
changePrivacy(list_name, state_name);
|
|
else
|
|
changePrivacy('', state_name);
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the error functions for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 02/04/11
|
|
|
|
*/
|
|
|
|
// Shows the given error output
|
|
function showError(condition, reason, type) {
|
|
// Enough data to output the error
|
|
if(condition || reason) {
|
|
// Initialize the error text
|
|
var eText = '';
|
|
|
|
// Any error condition
|
|
if(condition)
|
|
eText += condition;
|
|
|
|
// Any error type
|
|
if(type && eText)
|
|
eText += ' (' + type + ')';
|
|
|
|
// Any error reason
|
|
if(reason) {
|
|
if(eText)
|
|
eText += ' - ';
|
|
|
|
eText += reason;
|
|
}
|
|
|
|
// We reveal the error
|
|
openThisError(1);
|
|
|
|
// Create the error text
|
|
$('#board .one-board.error[data-id="1"] span').text(eText);
|
|
}
|
|
|
|
// Not enough data to output the error: output a generic board
|
|
else
|
|
openThisError(2);
|
|
}
|
|
|
|
// Handles the error from a packet and return true if any error
|
|
function handleError(packet) {
|
|
/* REF: http://xmpp.org/extensions/xep-0086.html */
|
|
|
|
// Initialize
|
|
var type, code, reason, condition;
|
|
var node = $(packet);
|
|
|
|
// First level error (connection error)
|
|
if(node.is('error')) {
|
|
// Get the value
|
|
code = node.attr('code');
|
|
|
|
// Specific error reason
|
|
switch(code) {
|
|
case '401':
|
|
reason = _e("Authorization failed");
|
|
break;
|
|
|
|
case '409':
|
|
reason = _e("Registration failed, please choose a different username");
|
|
break;
|
|
|
|
case '503':
|
|
reason = _e("Service unavailable");
|
|
break;
|
|
|
|
case '500':
|
|
reason = _e("Internal server error, try later");
|
|
break;
|
|
|
|
default:
|
|
reason = node.find('text').text();
|
|
break;
|
|
}
|
|
|
|
// Remove the general wait item (security)
|
|
removeGeneralWait();
|
|
|
|
// Show reconnect pane
|
|
if(CURRENT_SESSION && CONNECTED) {
|
|
// Anonymous?
|
|
if(isAnonymous())
|
|
createReconnect('anonymous');
|
|
else
|
|
createReconnect('normal');
|
|
}
|
|
|
|
// Show the homepage (security)
|
|
else if(!CURRENT_SESSION || !CONNECTED) {
|
|
$('#home').show();
|
|
pageTitle('home');
|
|
}
|
|
|
|
// Still connected? (security)
|
|
if(isConnected())
|
|
con.disconnect();
|
|
|
|
logThis('First level error received.', 1);
|
|
}
|
|
|
|
// Second level error (another error)
|
|
else if(node.find('error').size()) {
|
|
type = node.find('error').attr('type');
|
|
reason = node.find('error text').text();
|
|
condition = packet.getElementsByTagName('error').item(0).childNodes.item(0).nodeName.replace(/-/g, ' ');
|
|
|
|
logThis('Second level error received.', 1);
|
|
}
|
|
|
|
// No error
|
|
else
|
|
return false;
|
|
|
|
// Show the error board
|
|
showError(condition, reason, type);
|
|
|
|
// Return there's an error
|
|
return true;
|
|
}
|
|
|
|
// Handles the error reply of a packet
|
|
function handleErrorReply(packet) {
|
|
return handleError(packet.getNode());
|
|
}
|
|
|
|
// Handles the error reply for a message
|
|
function handleMessageError(packet) {
|
|
if(!handleErrorReply(packet))
|
|
handleMessage(packet);
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the buddy name related JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 29/04/11
|
|
|
|
*/
|
|
|
|
// Gets an user name for buddy add tool
|
|
function getAddUserName(xid) {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
iq.setTo(xid);
|
|
|
|
iq.appendNode('vCard', {'xmlns': NS_VCARD});
|
|
|
|
con.send(iq, handleAddUserName);
|
|
}
|
|
|
|
// Handles an user name for buddy add tool
|
|
function handleAddUserName(iq) {
|
|
// Was it an obsolete request?
|
|
if(!exists('.add-contact-name-get[data-for="' + escape(bareXID(getStanzaFrom(iq))) + '"]'))
|
|
return false;
|
|
|
|
// Reset the waiting item
|
|
$('.add-contact-name-get').hide().removeAttr('data-for');
|
|
|
|
// Get the names
|
|
if(iq.getType() == 'result') {
|
|
var full_name = generateBuddyName(iq)[0];
|
|
|
|
if(full_name)
|
|
$('.add-contact-name').val(full_name);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Generates the good buddy name from a vCard IQ reply
|
|
function generateBuddyName(iq) {
|
|
// Get the IQ content
|
|
var xml = $(iq.getNode()).find('vCard');
|
|
|
|
// Get the full name & the nickname
|
|
var pFull = xml.find('FN:first').text();
|
|
var pNick = xml.find('NICKNAME:first').text();
|
|
|
|
// No full name?
|
|
if(!pFull) {
|
|
// Get the given name
|
|
var pN = xml.find('N:first');
|
|
var pGiven = pN.find('GIVEN:first').text();
|
|
|
|
if(pGiven) {
|
|
pFull = pGiven;
|
|
|
|
// Get the family name (optional)
|
|
var pFamily = pN.find('FAMILY:first').text();
|
|
|
|
if(pFamily)
|
|
pFull += ' ' + pFamily;
|
|
}
|
|
}
|
|
|
|
return [pFull, pNick];
|
|
}
|
|
|
|
// Returns the given XID buddy name
|
|
function getBuddyName(xid) {
|
|
// Initialize
|
|
var cname, bname;
|
|
|
|
// Cut the XID resource
|
|
xid = bareXID(xid);
|
|
|
|
// This is me?
|
|
if(isAnonymous() && !xid)
|
|
bname = _e("You");
|
|
else if(xid == getXID())
|
|
bname = getName();
|
|
|
|
// Not me!
|
|
else {
|
|
cname = $('#buddy-list .buddy[data-xid="' + escape(xid) + '"]:first .buddy-name').html();
|
|
|
|
// If the complete name exists
|
|
if(cname)
|
|
bname = cname.revertHtmlEnc();
|
|
|
|
// Else, we just get the nickname of the buddy
|
|
else
|
|
bname = getXIDNick(xid);
|
|
}
|
|
|
|
return bname;
|
|
}
|
|
|
|
// Gets the nickname of the user
|
|
function getNick() {
|
|
try {
|
|
// Try to read the user nickname
|
|
var nick = getDB('profile', 'nick');
|
|
|
|
// No nick?
|
|
if(!nick)
|
|
nick = con.username;
|
|
|
|
return nick;
|
|
} catch(e) {}
|
|
}
|
|
|
|
// Gets the full name of the user
|
|
function getName() {
|
|
// Try to read the user name
|
|
var name = getDB('profile', 'name');
|
|
|
|
// No name? Use the nickname instead!
|
|
if(!name)
|
|
name = getNick();
|
|
|
|
return name;
|
|
}
|
|
|
|
// Gets the MUC nickname of the user
|
|
function getMUCNick(id) {
|
|
return unescape($('#' + id).attr('data-nick'));
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the favorites JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 14/05/13
|
|
|
|
*/
|
|
|
|
// Opens the favorites popup
|
|
function openFavorites() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Manage favorite rooms") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="switch-fav">' +
|
|
'<div class="room-switcher room-list">' +
|
|
'<div class="icon list-icon talk-images"></div>' +
|
|
|
|
_e("Change favorites") +
|
|
'</div>' +
|
|
|
|
'<div class="room-switcher room-search">' +
|
|
'<div class="icon search-icon talk-images"></div>' +
|
|
|
|
_e("Search a room") +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="static-fav">' +
|
|
'<div class="favorites-edit favorites-content">' +
|
|
'<div class="head fedit-head static-fav-head">' +
|
|
'<div class="head-text fedit-head-text">' + _e("Select a favorite") + '</div>' +
|
|
|
|
'<select name="fedit-head-select" class="head-select fedit-head-select"></select>' +
|
|
'</div>' +
|
|
|
|
'<div class="results fedit-results static-fav-results">' +
|
|
'<div class="fedit-line">' +
|
|
'<label>' + _e("Name") + '</label>' +
|
|
|
|
'<input class="fedit-title" type="text" required="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="fedit-line">' +
|
|
'<label>' + _e("Nickname") + '</label>' +
|
|
|
|
'<input class="fedit-nick" type="text" value="' + getNick() + '" required="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="fedit-line">' +
|
|
'<label>' + _e("Room") + '</label>' +
|
|
|
|
'<input class="fedit-chan" type="text" required="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="fedit-line">' +
|
|
'<label>' + _e("Server") + '</label>' +
|
|
|
|
'<input class="fedit-server" type="text" value="' + HOST_MUC + '" required="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="fedit-line">' +
|
|
'<label>' + _e("Password") + '</label>' +
|
|
|
|
'<input class="fedit-password" type="password" />' +
|
|
'</div>' +
|
|
|
|
'<div class="fedit-line">' +
|
|
'<label>' + _e("Automatic") + '</label>' +
|
|
|
|
'<input type="checkbox" class="fedit-autojoin" />' +
|
|
'</div>' +
|
|
|
|
'<div class="fedit-actions">' +
|
|
'<a href="#" class="fedit-terminate fedit-add add one-button talk-images">' + _e("Add") + '</a>' +
|
|
'<a href="#" class="fedit-terminate fedit-edit one-button talk-images">' + _e("Edit") + '</a>' +
|
|
'<a href="#" class="fedit-terminate fedit-remove remove one-button talk-images">' + _e("Remove") + '</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="favorites-search favorites-content">' +
|
|
'<div class="head fsearch-head static-fav-head">' +
|
|
'<div class="head-text fsearch-head-text">' + _e("Search a room on") + '</div>' +
|
|
|
|
'<input type="text" class="head-input fsearch-head-server" value="' + HOST_MUC + '" />' +
|
|
'</div>' +
|
|
|
|
'<div class="results fsearch-results static-fav-results">' +
|
|
'<p class="fsearch-noresults">' + _e("No room found on this server.") + '</p>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('favorites', html);
|
|
|
|
// Load the favorites
|
|
loadFavorites();
|
|
|
|
// Associate the events
|
|
launchFavorites();
|
|
}
|
|
|
|
// Resets the favorites elements
|
|
function resetFavorites() {
|
|
var path = '#favorites ';
|
|
|
|
$(path + '.wait, ' + path + '.fedit-terminate').hide();
|
|
$(path + '.fedit-add').show();
|
|
$(path + '.fsearch-oneresult').remove();
|
|
$(path + 'input').val('');
|
|
$(path + '.please-complete').removeClass('please-complete');
|
|
$(path + '.fedit-nick').val(getNick());
|
|
$(path + '.fsearch-head-server, ' + path + '.fedit-server').val(HOST_MUC);
|
|
$(path + '.fedit-autojoin').removeAttr('checked');
|
|
}
|
|
|
|
// Quits the favorites popup
|
|
function quitFavorites() {
|
|
// Destroy the popup
|
|
destroyPopup('favorites');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Adds a room to the favorites
|
|
function addThisFavorite(roomXID, roomName) {
|
|
// Button path
|
|
var button = '#favorites .fsearch-results div[data-xid="' + escape(roomXID) + '"] a.one-button';
|
|
|
|
// Add a remove button instead of the add one
|
|
$(button + '.add').replaceWith('<a href="#" class="one-button remove talk-images">' + _e("Remove") + '</a>');
|
|
|
|
// Click event
|
|
$(button + '.remove').click(function() {
|
|
return removeThisFavorite(roomXID, roomName);
|
|
});
|
|
|
|
// Hide the add button in the (opened?) groupchat
|
|
$('#' + hex_md5(roomXID) + ' .tools-add').hide();
|
|
|
|
// Add the database entry
|
|
displayFavorites(roomXID, explodeThis(' (', roomName, 0), getNick(), '0', '');
|
|
|
|
// Publish the favorites
|
|
favoritePublish();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Removes a room from the favorites
|
|
function removeThisFavorite(roomXID, roomName) {
|
|
// Button path
|
|
var button = '#favorites .fsearch-results div[data-xid="' + escape(roomXID) + '"] a.one-button';
|
|
|
|
// Add a remove button instead of the add one
|
|
$(button + '.remove').replaceWith('<a href="#" class="one-button add talk-images">' + _e("Add") + '</a>');
|
|
|
|
// Click event
|
|
$(button + '.add').click(function() {
|
|
return addThisFavorite(roomXID, roomName);
|
|
});
|
|
|
|
// Show the add button in the (opened?) groupchat
|
|
$('#' + hex_md5(roomXID) + ' .tools-add').show();
|
|
|
|
// Remove the favorite
|
|
removeFavorite(roomXID, true);
|
|
|
|
// Publish the favorites
|
|
favoritePublish();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Edits a favorite
|
|
function editFavorite() {
|
|
// Path to favorites
|
|
var favorites = '#favorites .';
|
|
|
|
// Reset the favorites
|
|
resetFavorites();
|
|
|
|
// Show the edit/remove button, hide the others
|
|
$(favorites + 'fedit-terminate').hide();
|
|
$(favorites + 'fedit-edit').show();
|
|
$(favorites + 'fedit-remove').show();
|
|
|
|
// We retrieve the values
|
|
var xid = $(favorites + 'fedit-head-select').val();
|
|
var data = XMLFromString(getDB('favorites', xid));
|
|
|
|
// If this is not the default room
|
|
if(xid != 'none') {
|
|
// We apply the values
|
|
$(favorites + 'fedit-title').val($(data).find('name').text());
|
|
$(favorites + 'fedit-nick').val($(data).find('nick').text());
|
|
$(favorites + 'fedit-chan').val(getXIDNick(xid));
|
|
$(favorites + 'fedit-server').val(getXIDHost(xid));
|
|
$(favorites + 'fedit-password').val($(data).find('password').text());
|
|
|
|
if($(data).find('autojoin').text() == 'true')
|
|
$(favorites + 'fedit-autojoin').attr('checked', true);
|
|
}
|
|
}
|
|
|
|
// Adds a favorite
|
|
function addFavorite() {
|
|
// Path to favorites
|
|
var favorites = '#favorites .';
|
|
|
|
// We reset the inputs
|
|
$(favorites + 'fedit-title, ' + favorites + 'fedit-nick, ' + favorites + 'fedit-chan, ' + favorites + 'fedit-server, ' + favorites + 'fedit-password').val('');
|
|
|
|
// Show the add button, hide the others
|
|
$(favorites + 'fedit-terminate').hide();
|
|
$(favorites + 'fedit-add').show();
|
|
}
|
|
|
|
// Terminate a favorite editing
|
|
function terminateThisFavorite(type) {
|
|
// Path to favorites
|
|
var favorites = '#favorites ';
|
|
|
|
// We get the values of the current edited groupchat
|
|
var old_xid = $(favorites + '.fedit-head-select').val();
|
|
|
|
var title = $(favorites + '.fedit-title').val();
|
|
var nick = $(favorites + '.fedit-nick').val();
|
|
var room = $(favorites + '.fedit-chan').val();
|
|
var server = $(favorites + '.fedit-server').val();
|
|
var xid = room + '@' + server;
|
|
var password = $(favorites + '.fedit-password').val();
|
|
var autojoin = 'false';
|
|
|
|
if($(favorites + '.fedit-autojoin').filter(':checked').size())
|
|
autojoin = 'true';
|
|
|
|
// We check the missing values and send this if okay
|
|
if((type == 'add') || (type == 'edit')) {
|
|
if(title && nick && room && server) {
|
|
// Remove the edited room
|
|
if(type == 'edit')
|
|
removeFavorite(old_xid, true);
|
|
|
|
// Display the favorites
|
|
displayFavorites(xid, title, nick, autojoin, password);
|
|
|
|
// Reset the inputs
|
|
resetFavorites();
|
|
}
|
|
|
|
else {
|
|
$(favorites + 'input[required]').each(function() {
|
|
var select = $(this);
|
|
|
|
if(!select.val())
|
|
$(document).oneTime(10, function() {
|
|
select.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
select.removeClass('please-complete');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Must remove a favorite?
|
|
else if(type == 'remove') {
|
|
removeFavorite(old_xid, true);
|
|
|
|
// Reset the inputs
|
|
resetFavorites();
|
|
}
|
|
|
|
// Publish the new favorites
|
|
favoritePublish();
|
|
|
|
logThis('Action on this bookmark: ' + room + '@' + server + ' / ' + type, 3);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Removes a favorite
|
|
function removeFavorite(xid, database) {
|
|
// We remove the target favorite everywhere needed
|
|
$('.buddy-conf-groupchat-select option[value="' + xid + '"]').remove();
|
|
$('.fedit-head-select option[value="' + xid + '"]').remove();
|
|
|
|
// Must remove it from database?
|
|
if(database)
|
|
removeDB('favorites', xid);
|
|
}
|
|
|
|
// Sends a favorite to the XMPP server
|
|
function favoritePublish() {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var query = iq.setQuery(NS_PRIVATE);
|
|
var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_BOOKMARKS}));
|
|
|
|
// We generate the XML
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored favorite
|
|
if(explodeThis('_', current, 0) == 'favorites') {
|
|
var data = XMLFromString(storageDB.getItem(current));
|
|
var xid = $(data).find('xid').text();
|
|
var rName = $(data).find('name').text();
|
|
var nick = $(data).find('nick').text();
|
|
var password = $(data).find('password').text();
|
|
var autojoin = $(data).find('autojoin').text();
|
|
|
|
// We create the node for this groupchat
|
|
var item = storage.appendChild(iq.buildNode('conference', {'name': rName, 'jid': xid, 'autojoin': autojoin, xmlns: NS_BOOKMARKS}));
|
|
item.appendChild(iq.buildNode('nick', {xmlns: NS_BOOKMARKS}, nick));
|
|
|
|
if(password)
|
|
item.appendChild(iq.buildNode('password', {xmlns: NS_BOOKMARKS}, password));
|
|
|
|
logThis('Bookmark sent: ' + xid, 3);
|
|
}
|
|
}
|
|
|
|
con.send(iq);
|
|
}
|
|
|
|
// Gets a list of the MUC items on a given server
|
|
function getGCList() {
|
|
var path = '#favorites .';
|
|
var gcServer = $('.fsearch-head-server').val();
|
|
|
|
// We reset some things
|
|
$(path + 'fsearch-oneresult').remove();
|
|
$(path + 'fsearch-noresults').hide();
|
|
$(path + 'wait').show();
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
iq.setTo(gcServer);
|
|
|
|
iq.setQuery(NS_DISCO_ITEMS);
|
|
|
|
con.send(iq, handleGCList);
|
|
}
|
|
|
|
// Handles the MUC items list
|
|
function handleGCList(iq) {
|
|
var path = '#favorites .';
|
|
var from = fullXID(getStanzaFrom(iq));
|
|
|
|
if (!iq || (iq.getType() != 'result')) {
|
|
openThisError(3);
|
|
|
|
$(path + 'wait').hide();
|
|
|
|
logThis('Error while retrieving the rooms: ' + from, 1);
|
|
}
|
|
|
|
else {
|
|
var handleXML = iq.getQuery();
|
|
|
|
if($(handleXML).find('item').size()) {
|
|
// Initialize the HTML code
|
|
var html = '';
|
|
|
|
$(handleXML).find('item').each(function() {
|
|
var roomXID = $(this).attr('jid');
|
|
var roomName = $(this).attr('name');
|
|
|
|
if(roomXID && roomName) {
|
|
// Escaped values
|
|
var escaped_xid = encodeOnclick(roomXID);
|
|
var escaped_name = encodeOnclick(roomName);
|
|
|
|
// Initialize the room HTML
|
|
html += '<div class="oneresult fsearch-oneresult" data-xid="' + escape(roomXID) + '">' +
|
|
'<div class="room-name">' + roomName.htmlEnc() + '</div>' +
|
|
'<a href="#" class="one-button join talk-images" onclick="return joinFavorite(\'' + escaped_xid + '\');">' + _e("Join") + '</a>';
|
|
|
|
// This room is yet a favorite
|
|
if(existDB('favorites', roomXID))
|
|
html += '<a href="#" class="one-button remove talk-images" onclick="return removeThisFavorite(\'' + escaped_xid + '\', \'' + escaped_name + '\');">' + _e("Remove") + '</a>';
|
|
else
|
|
html += '<a href="#" class="one-button add talk-images" onclick="return addThisFavorite(\'' + escaped_xid + '\', \'' + escaped_name + '\');">' + _e("Add") + '</a>';
|
|
|
|
// Close the room HTML
|
|
html += '</div>';
|
|
}
|
|
});
|
|
|
|
// Append this code to the popup
|
|
$(path + 'fsearch-results').append(html);
|
|
}
|
|
|
|
else
|
|
$(path + 'fsearch-noresults').show();
|
|
|
|
logThis('Rooms retrieved: ' + from, 3);
|
|
}
|
|
|
|
$(path + 'wait').hide();
|
|
}
|
|
|
|
// Joins a groupchat from favorites
|
|
function joinFavorite(room) {
|
|
quitFavorites();
|
|
checkChatCreate(room, 'groupchat', '', '', getXIDNick(room));
|
|
|
|
return false;
|
|
}
|
|
|
|
// Displays a given favorite
|
|
function displayFavorites(xid, name, nick, autojoin, password) {
|
|
// Generate the HTML code
|
|
var html = '<option value="' + encodeQuotes(xid) + '">' + name.htmlEnc() + '</option>';
|
|
|
|
// Remove the existing favorite
|
|
removeFavorite(xid, false);
|
|
|
|
// We complete the select forms
|
|
$('#buddy-list .gc-join-first-option, #favorites .fedit-head-select-first-option').after(html);
|
|
|
|
// We store the informations
|
|
var value = '<groupchat><xid>' + xid.htmlEnc() + '</xid><name>' + name.htmlEnc() + '</name><nick>' + nick.htmlEnc() + '</nick><autojoin>' + autojoin.htmlEnc() + '</autojoin><password>' + password.htmlEnc() + '</password></groupchat>';
|
|
setDB('favorites', xid, value);
|
|
}
|
|
|
|
// Loads the favorites for the popup
|
|
function loadFavorites() {
|
|
// Initialize the HTML code
|
|
var html = '';
|
|
|
|
// Read the database
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored favorite
|
|
if(explodeThis('_', current, 0) == 'favorites') {
|
|
var data = XMLFromString(storageDB.getItem(current));
|
|
|
|
// Add the current favorite to the HTML code
|
|
html += '<option value="' + encodeQuotes($(data).find('xid').text()) + '">' + $(data).find('name').text().htmlEnc() + '</option>';
|
|
}
|
|
}
|
|
|
|
// Generate specific HTML code
|
|
var favorites_bubble = '<option value="none" class="gc-join-first-option" selected="">' + _e("Select a favorite") + '</option>' + html;
|
|
var favorites_popup = '<option value="none" class="fedit-head-select-first-option" selected="">' + _e("Select a favorite") + '</option>' + html;
|
|
|
|
// Append the HTML code
|
|
$('#buddy-list .buddy-conf-groupchat-select').html(favorites_bubble);
|
|
$('#favorites .fedit-head-select').html(favorites_popup);
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchFavorites() {
|
|
var path = '#favorites .';
|
|
|
|
// Keyboard events
|
|
$(path + 'fsearch-head-server').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// No value?
|
|
if(!$(this).val())
|
|
$(this).val(HOST_MUC);
|
|
|
|
// Get the list
|
|
getGCList();
|
|
}
|
|
});
|
|
|
|
$(path + 'fedit-line input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// Edit a favorite
|
|
if($(path + 'fedit-edit').is(':visible'))
|
|
terminateThisFavorite('edit');
|
|
|
|
// Add a favorite
|
|
else
|
|
terminateThisFavorite('add');
|
|
}
|
|
});
|
|
|
|
// Change events
|
|
$('.fedit-head-select').change(editFavorite);
|
|
|
|
// Click events
|
|
$(path + 'room-switcher').click(function() {
|
|
$(path + 'favorites-content').hide();
|
|
resetFavorites();
|
|
});
|
|
|
|
$(path + 'room-list').click(function() {
|
|
$(path + 'favorites-edit').show();
|
|
});
|
|
|
|
$(path + 'room-search').click(function() {
|
|
$(path + 'favorites-search').show();
|
|
getGCList();
|
|
});
|
|
|
|
$(path + 'fedit-add').click(function() {
|
|
return terminateThisFavorite('add');
|
|
});
|
|
|
|
$(path + 'fedit-edit').click(function() {
|
|
return terminateThisFavorite('edit');
|
|
});
|
|
|
|
$(path + 'fedit-remove').click(function() {
|
|
return terminateThisFavorite('remove');
|
|
});
|
|
|
|
$(path + 'bottom .finish').click(function() {
|
|
return quitFavorites();
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
This is the server features JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou, Maranda
|
|
Last revision: 07/06/13
|
|
|
|
*/
|
|
|
|
// Gets the features of a server
|
|
function getFeatures() {
|
|
/* REF: http://xmpp.org/extensions/xep-0030.html */
|
|
|
|
// Get the main values
|
|
var to = getServer();
|
|
var caps = con.server_caps;
|
|
var xml = null;
|
|
|
|
// Try to get the stored data
|
|
if(caps)
|
|
xml = XMLFromString(getPersistent('global', 'caps', caps));
|
|
|
|
// Any stored data?
|
|
if(xml) {
|
|
handleFeatures(xml);
|
|
|
|
logThis('Read server CAPS from cache.');
|
|
}
|
|
|
|
// Not stored (or no CAPS)!
|
|
else {
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setTo(to);
|
|
iq.setType('get');
|
|
iq.setQuery(NS_DISCO_INFO);
|
|
|
|
con.send(iq, handleDiscoInfos);
|
|
|
|
logThis('Read server CAPS from network.');
|
|
}
|
|
}
|
|
|
|
// Handles the features of a server
|
|
function handleFeatures(xml) {
|
|
// Selector
|
|
var selector = $(xml);
|
|
|
|
// Markers
|
|
var pep = false;
|
|
var pubsub = false;
|
|
var pubsub_cn = false;
|
|
var mam = false;
|
|
var commands = false;
|
|
|
|
// Scan the features
|
|
if(selector.find('identity[category="pubsub"][type="pep"]').size())
|
|
pep = true;
|
|
if(selector.find('feature[var="' + NS_PUBSUB + '"]').size())
|
|
pubsub = true;
|
|
if(selector.find('feature[var="' + NS_PUBSUB_CN + '"]').size())
|
|
pubsub_cn = true;
|
|
if(selector.find('feature[var="' + NS_URN_MAM + '"]').size())
|
|
mam = true;
|
|
if(selector.find('feature[var="' + NS_COMMANDS + '"]').size())
|
|
commands = true;
|
|
|
|
// Enable the pep elements if available
|
|
if(pep) {
|
|
// Update our database
|
|
enableFeature('pep');
|
|
|
|
// Get the PEP nodes to initiate
|
|
getInitMicroblog();
|
|
getInitGeoloc();
|
|
|
|
// Get the notifications
|
|
getNotifications();
|
|
|
|
// Geolocate the user
|
|
geolocate();
|
|
|
|
// Enable microblogging send tools
|
|
waitMicroblog('sync');
|
|
$('.postit.attach').css('display', 'block');
|
|
|
|
logThis('XMPP server supports PEP.', 3);
|
|
}
|
|
|
|
// Disable microblogging send tools (no PEP!)
|
|
else {
|
|
waitMicroblog('unsync');
|
|
|
|
logThis('XMPP server does not support PEP.', 2);
|
|
}
|
|
|
|
// Enable the pubsub features if available
|
|
if(pubsub)
|
|
enableFeature(NS_PUBSUB);
|
|
|
|
// Enable the pubsub config-node features if available
|
|
if(pubsub_cn)
|
|
enableFeature(NS_PUBSUB_CN);
|
|
|
|
// Enable the message MAM management features if available
|
|
if(mam)
|
|
enableFeature(NS_URN_MAM);
|
|
|
|
// Enable the commands features if available
|
|
if(commands)
|
|
enableFeature(NS_COMMANDS);
|
|
|
|
// Hide the private life fieldset if nothing to show
|
|
if(!pep && !mam)
|
|
$('#options fieldset.privacy').hide();
|
|
|
|
// Apply the features
|
|
applyFeatures('talk');
|
|
|
|
// Process the buddy-list height
|
|
if(pep)
|
|
adaptRoster();
|
|
|
|
return false;
|
|
}
|
|
|
|
// The function to apply the features to an element
|
|
function applyFeatures(id) {
|
|
// Path to the elements
|
|
var path = '#' + id + ' .';
|
|
|
|
// PEP features
|
|
if(enabledPEP())
|
|
$(path + 'pep-hidable').show();
|
|
|
|
// PubSub features
|
|
if(enabledPubSub())
|
|
$(path + 'pubsub-hidable').show();
|
|
|
|
// PubSub Config-Node features
|
|
if(enabledPubSubCN())
|
|
$(path + 'pubsub-hidable-cn').show();
|
|
|
|
// MAM features
|
|
if(enabledMAM())
|
|
$(path + 'mam-hidable').show();
|
|
|
|
// Commands features
|
|
if(enabledCommands())
|
|
$(path + 'commands-hidable').show();
|
|
|
|
// XMPP links (browser feature)
|
|
if(navigator.registerProtocolHandler)
|
|
$(path + 'xmpplinks-hidable').show();
|
|
}
|
|
|
|
// Enables a feature
|
|
function enableFeature(feature) {
|
|
setDB('feature', feature, 'true');
|
|
}
|
|
|
|
// Checks if a feature is enabled
|
|
function enabledFeature(feature) {
|
|
if(getDB('feature', feature) == 'true')
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// Returns the XMPP server PEP support
|
|
function enabledPEP() {
|
|
return enabledFeature('pep');
|
|
}
|
|
|
|
// Returns the XMPP server PubSub support
|
|
function enabledPubSub() {
|
|
return enabledFeature(NS_PUBSUB);
|
|
}
|
|
|
|
// Returns the XMPP server PubSub Config-Node support
|
|
function enabledPubSubCN() {
|
|
return enabledFeature(NS_PUBSUB_CN);
|
|
}
|
|
|
|
// Returns the XMPP server MAM support
|
|
function enabledMAM() {
|
|
return enabledFeature(NS_URN_MAM);
|
|
}
|
|
|
|
// Returns the XMPP server commands support
|
|
function enabledCommands() {
|
|
return enabledFeature(NS_COMMANDS);
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the interface JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 23/01/13
|
|
|
|
*/
|
|
|
|
// Changes the title of the document
|
|
function pageTitle(title) {
|
|
// Anonymous mode?
|
|
var head_name = getName();
|
|
|
|
if(isAnonymous())
|
|
head_name = ANONYMOUS_ROOM + ' (' + _e("anonymous mode") + ')';
|
|
|
|
// We change the title to give essential informations
|
|
switch(title) {
|
|
case 'home':
|
|
document.title = SERVICE_NAME + ' • ' + SERVICE_DESC;
|
|
|
|
break;
|
|
|
|
case 'talk':
|
|
document.title = SERVICE_NAME + ' • ' + head_name;
|
|
|
|
break;
|
|
|
|
case 'new':
|
|
document.title = '[' + pendingEvents() + '] ' + SERVICE_NAME + ' • ' + head_name;
|
|
|
|
break;
|
|
|
|
case 'wait':
|
|
document.title = SERVICE_NAME + ' • ' + _e("Please wait...");
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Creates a general-wait item
|
|
function showGeneralWait() {
|
|
// Item exists?
|
|
if(exists('#general-wait'))
|
|
return false;
|
|
|
|
// Generate the HTML code
|
|
var html =
|
|
'<div id="general-wait" class="removable">' +
|
|
'<div class="general-wait-content wait-big"></div>' +
|
|
'</div>';
|
|
|
|
// Append the HTML code
|
|
$('body').append(html);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Removes the general-wait item
|
|
function removeGeneralWait() {
|
|
$('#general-wait').remove();
|
|
}
|
|
|
|
// Generates a file upload valid form content
|
|
function generateFileShare() {
|
|
return '<input type="hidden" name="MAX_FILE_SIZE" value="' + encodeQuotes(JAPPIX_MAX_FILE_SIZE) + '">' +
|
|
'<input type="hidden" name="user" value="' + encodeQuotes(getXID()) + '" />' +
|
|
'<input type="hidden" name="location" value="' + encodeQuotes(generateURL(JAPPIX_LOCATION)) + '" />' +
|
|
'<input type="hidden" name="id" value="' + (new Date()).getTime() + '" />' +
|
|
'<input type="file" name="file" required="" />' +
|
|
'<input type="submit" value="' + _e("Send") + '" />';
|
|
}
|
|
|
|
// Switches to the given chan
|
|
function switchChan(id) {
|
|
if(exists('#' + id)) {
|
|
// We show the page-engine content
|
|
$('.page-engine-chan').hide();
|
|
$('#' + id).show();
|
|
|
|
// We edit the tab switcher
|
|
$('#page-switch .switcher').removeClass('activechan').addClass('chan');
|
|
$('#page-switch .' + id).addClass('activechan').removeClass('chan');
|
|
|
|
// Scroll down to the last message
|
|
if(id != 'channel')
|
|
autoScroll(id);
|
|
|
|
// Manage input focus
|
|
inputFocus();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Loads the complete chat switcher
|
|
function loadChatSwitch() {
|
|
// Path
|
|
var more_content = '#page-switch .more-content';
|
|
|
|
// Yet displayed?
|
|
if(exists(more_content))
|
|
return closeBubbles();
|
|
|
|
// Add the bubble
|
|
showBubble(more_content);
|
|
|
|
// Append the content
|
|
$('#page-switch .more').append(
|
|
'<div class="more-content bubble removable">' +
|
|
$('#page-switch .chans').html() +
|
|
'</div>'
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Puts the selected smiley in the good page-engine input
|
|
function insertSmiley(smiley, hash) {
|
|
// We define the variables
|
|
var selector = $('#' + hash + ' .message-area');
|
|
var oValue = selector.val();
|
|
|
|
// Any old value?
|
|
if(oValue && !oValue.match(/^(.+)(\s)+$/))
|
|
oValue += ' ';
|
|
|
|
var nValue = oValue + smiley + ' ';
|
|
|
|
// Put the new value and focus on it
|
|
$(document).oneTime(10, function() {
|
|
selector.val(nValue).focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Deletes all the associated elements of the chat we want to remove
|
|
function deleteThisChat(hash) {
|
|
$('#' + hash + ', #page-switch .' + hash).remove();
|
|
}
|
|
|
|
// Closes the given chat
|
|
function quitThisChat(xid, hash, type) {
|
|
if(type == 'groupchat') {
|
|
// Send our unavailable presence
|
|
sendPresence(xid + '/' + getMUCNick(hash), 'unavailable');
|
|
|
|
// Remove all presence database entries for this groupchat
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
var cXID = explodeThis('_', current, 1);
|
|
|
|
// If the pointer is on a presence from this groupchat
|
|
if((explodeThis('_', current, 0) == 'presence') && (bareXID(cXID) == xid)) {
|
|
// Generate the hash for the current XID
|
|
var cHash = hex_md5(cXID);
|
|
|
|
// Disable the message textarea
|
|
$('#' + cHash + ' .message-area').attr('disabled', true);
|
|
|
|
// Remove the presence for this XID
|
|
removeDB('presence-stanza', cXID);
|
|
removeDB('presence-resources', cXID);
|
|
presenceFunnel(cXID, cHash);
|
|
}
|
|
}
|
|
} else {
|
|
chatStateSend('gone', xid, hash);
|
|
}
|
|
|
|
// Clear MAM storage for this chat
|
|
if(xid in MAM_MAP_STATES) {
|
|
delete MAM_MAP_STATES[xid];
|
|
}
|
|
|
|
// Get the chat ID which is before
|
|
var previous = $('#' + hash).prev().attr('id');
|
|
|
|
// Remove the chat
|
|
deleteThisChat(hash);
|
|
|
|
// Reset the switcher
|
|
if(!exists('#page-switch .switcher.activechan')) {
|
|
switchChan(previous);
|
|
}
|
|
|
|
// Reset the notifications
|
|
chanCleanNotify(hash);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Generates the chat logs
|
|
function generateChatLog(xid, hash) {
|
|
// Get the main values
|
|
var path = '#' + hash + ' .';
|
|
var content = $(path + 'content').clone().contents();
|
|
var avatar = $(path + 'top .avatar-container:first').html();
|
|
var nick = $(path + 'top .bc-name').text();
|
|
var date = getXMPPTime('local');
|
|
var type = $('#' + hash).attr('data-type');
|
|
var direction = $('html').attr('dir') || 'ltr';
|
|
|
|
// Filter the content smileys
|
|
$(content).find('img.emoticon').each(function() {
|
|
$(this).replaceWith($(this).attr('alt'));
|
|
});
|
|
|
|
// Remove the useless attributes
|
|
$(content).removeAttr('data-type').removeAttr('data-stamp');
|
|
|
|
// Remove the content avatars
|
|
$(content).find('.avatar-container').remove();
|
|
|
|
// Remove the content click events
|
|
$(content).find('a').removeAttr('onclick');
|
|
|
|
// Extract the content HTML code
|
|
content = $(content).parent().html();
|
|
|
|
// No avatar?
|
|
if(!avatar || !avatar.match(/data:/))
|
|
avatar = 'none';
|
|
|
|
// POST the values to the server
|
|
$.post('./php/generate-chat.php', { 'content': content, 'xid': xid, 'nick': nick, 'avatar': avatar, 'date': date, 'type': type, 'direction': direction }, function(data) {
|
|
// Handled!
|
|
$(path + 'tooltip-waitlog').replaceWith('<a class="tooltip-actionlog" href="./php/download-chat.php?id=' + data + '" target="_blank">' + _e("Download file!") + '</a>');
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Notifies the user from a new incoming message
|
|
var CHAT_FOCUS_HASH = null;
|
|
|
|
function messageNotify(hash, type) {
|
|
// Initialize the vars
|
|
var chat_switch = '#page-switch .';
|
|
var tested = chat_switch + hash;
|
|
var active = $(tested).hasClass('activechan');
|
|
|
|
// We notify the user if he has not the focus on the chat
|
|
if(!active || !isFocused() || (CHAT_FOCUS_HASH != hash)) {
|
|
if(!active) {
|
|
if(type == 'personal')
|
|
$(tested + ', ' + chat_switch + 'more-button').addClass('chan-newmessage');
|
|
else if(type == 'unread')
|
|
$(tested).addClass('chan-unread');
|
|
}
|
|
|
|
// Count the number of pending messages
|
|
var pending = 1;
|
|
|
|
if(exists('#' + hash + '[data-counter]'))
|
|
pending = parseInt($('#' + hash).attr('data-counter')) + 1;
|
|
|
|
$('#' + hash).attr('data-counter', pending);
|
|
}
|
|
|
|
// Update the page title
|
|
updateTitle();
|
|
}
|
|
|
|
// Returns the number of pending events
|
|
function pendingEvents() {
|
|
// Count the number of notifications
|
|
var number = 0;
|
|
|
|
$('.one-counter[data-counter]').each(function() {
|
|
number = number + parseInt($(this).attr('data-counter'));
|
|
});
|
|
|
|
return number;
|
|
}
|
|
|
|
// Updates the page title
|
|
function updateTitle() {
|
|
// Any pending events?
|
|
if(exists('.one-counter[data-counter]'))
|
|
pageTitle('new');
|
|
else
|
|
pageTitle('talk');
|
|
}
|
|
|
|
// Cleans the given chat notifications
|
|
function chanCleanNotify(hash) {
|
|
// We remove the class that tell the user of a new message
|
|
var chat_switch = '#page-switch .';
|
|
$(chat_switch + hash).removeClass('chan-newmessage chan-unread');
|
|
|
|
// We reset the global notifications if no more unread messages
|
|
if(!$(chat_switch + 'chans .chan-newmessage').size())
|
|
$(chat_switch + 'more-button').removeClass('chan-newmessage');
|
|
|
|
// We reset the chat counter
|
|
$('#' + hash).removeAttr('data-counter');
|
|
|
|
// Update the page title
|
|
updateTitle();
|
|
}
|
|
|
|
// Scrolls to the last chat message
|
|
function autoScroll(hash) {
|
|
// Avoid a JS error
|
|
if(exists('#' + hash)) {
|
|
var container = document.getElementById('chat-content-' + hash);
|
|
|
|
// Scroll down!
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// Shows all the buddies in the buddy-list
|
|
function showAllBuddies(from) {
|
|
// Put a marker
|
|
BLIST_ALL = true;
|
|
|
|
// We switch the two modes
|
|
$('.buddy-conf-more-display-unavailable').hide();
|
|
$('.buddy-conf-more-display-available').show();
|
|
|
|
// Security: reset all the groups toggle event
|
|
$('#buddy-list .group-buddies').show();
|
|
$('#buddy-list .group span').text('-');
|
|
|
|
// We show the disconnected buddies
|
|
$('.hidden-buddy').show();
|
|
|
|
// We show all the groups
|
|
$('#buddy-list .one-group').show();
|
|
|
|
if(SEARCH_FILTERED)
|
|
funnelFilterBuddySearch();
|
|
|
|
// Store this in the options
|
|
if((from == 'roster') && loadedOptions()) {
|
|
setDB('options', 'roster-showall', '1');
|
|
storeOptions();
|
|
}
|
|
}
|
|
|
|
// Shows only the online buddies in the buddy-list
|
|
function showOnlineBuddies(from) {
|
|
// Remove the marker
|
|
BLIST_ALL = false;
|
|
|
|
// We switch the two modes
|
|
$('.buddy-conf-more-display-available').hide();
|
|
$('.buddy-conf-more-display-unavailable').show();
|
|
|
|
// Security: reset all the groups toggle event
|
|
$('#buddy-list .group-buddies').show();
|
|
$('#buddy-list .group span').text('-');
|
|
|
|
// We hide the disconnected buddies
|
|
$('.hidden-buddy').hide();
|
|
|
|
// We check the groups to hide
|
|
updateGroups();
|
|
|
|
if(SEARCH_FILTERED)
|
|
funnelFilterBuddySearch();
|
|
|
|
// Store this in the options
|
|
if((from == 'roster') && loadedOptions()) {
|
|
setDB('options', 'roster-showall', '0');
|
|
storeOptions();
|
|
}
|
|
}
|
|
|
|
// Focuses on the right input
|
|
function inputFocus() {
|
|
// No popup shown
|
|
if(!exists('.popup'))
|
|
$(document).oneTime(10, function() {
|
|
$('.focusable:visible:first').focus();
|
|
});
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchInterface() {
|
|
// Focus on the first visible input
|
|
$(window).focus(inputFocus);
|
|
}
|
|
|
|
// Launch this plugin!
|
|
$(document).ready(launchInterface);
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the XMPP links handling JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 12/08/12
|
|
|
|
*/
|
|
|
|
// Does an action with the provided XMPP link
|
|
function xmppLink(link) {
|
|
/* REF: http://xmpp.org/registrar/querytypes.html */
|
|
|
|
// Remove the "xmpp:" string
|
|
link = explodeThis(':', link, 1);
|
|
|
|
// The XMPP URI has no "?"
|
|
if(link.indexOf('?') == -1)
|
|
checkChatCreate(link, 'chat');
|
|
|
|
// Parse the URI
|
|
else {
|
|
var xid = explodeThis('?', link, 0);
|
|
var action = explodeThis('?', link, 1);
|
|
|
|
switch(action) {
|
|
// Groupchat
|
|
case 'join':
|
|
checkChatCreate(xid, 'groupchat');
|
|
|
|
break;
|
|
|
|
// Profile
|
|
case 'vcard':
|
|
openUserInfos(xid);
|
|
|
|
break;
|
|
|
|
// Subscription
|
|
case 'subscribe':
|
|
addThisContact(xid);
|
|
|
|
break;
|
|
|
|
// Unsubscription
|
|
case 'unsubscribe':
|
|
sendRoster(xid, 'remove');
|
|
|
|
break;
|
|
|
|
// Private chat
|
|
default:
|
|
checkChatCreate(xid, 'chat');
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the links vars (get parameters in URL)
|
|
var LINK_VARS = (function() {
|
|
var vars = [], hash;
|
|
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
|
|
|
|
for(var i = 0; i < hashes.length; i++) {
|
|
var hash = hashes[i].split('=');
|
|
vars.push(hash[0]);
|
|
vars[hash[0]] = trim(decodeURIComponent(hash[1]));
|
|
}
|
|
|
|
return vars;
|
|
})();
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the IQ JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 27/08/11
|
|
|
|
*/
|
|
|
|
// Handles an incoming IQ packet
|
|
function handleIQ(iq) {
|
|
// Gets the IQ content
|
|
var iqNode = iq.getNode();
|
|
var iqFrom = fullXID(getStanzaFrom(iq));
|
|
var iqID = iq.getID();
|
|
var iqQueryXMLNS = iq.getQueryXMLNS();
|
|
var iqQuery = iq.getQuery();
|
|
var iqType = iq.getType();
|
|
|
|
// Build the response
|
|
var iqResponse = new JSJaCIQ();
|
|
|
|
iqResponse.setID(iqID);
|
|
iqResponse.setTo(iqFrom);
|
|
iqResponse.setType('result');
|
|
|
|
// OOB request
|
|
if((iqQueryXMLNS == NS_IQOOB) && (iqType == 'set')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0066.html */
|
|
|
|
handleOOB(iqFrom, iqID, 'iq', iqNode);
|
|
|
|
logThis('Received IQ OOB request: ' + iqFrom);
|
|
}
|
|
|
|
// OOB reply
|
|
else if(getDB('send/url', iqID)) {
|
|
// Get the values
|
|
var oob_url = getDB('send/url', iqID);
|
|
var oob_desc = getDB('send/desc', iqID);
|
|
var notif_id = hex_md5(oob_url + oob_desc + iqType + iqFrom + iqID);
|
|
|
|
// Error?
|
|
if($(iqNode).find('error').size()) {
|
|
// Rejected?
|
|
if($(iqNode).find('error not-acceptable').size())
|
|
newNotification('send_reject', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id);
|
|
|
|
// Failed?
|
|
else
|
|
newNotification('send_fail', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id);
|
|
|
|
// Remove the file
|
|
$.get(oob_url + '&action=remove');
|
|
}
|
|
|
|
// Success?
|
|
else if(iqType == 'result')
|
|
newNotification('send_accept', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id);
|
|
}
|
|
|
|
// Software version query
|
|
else if((iqQueryXMLNS == NS_VERSION) && (iqType == 'get')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0092.html */
|
|
|
|
var iqQuery = iqResponse.setQuery(NS_VERSION);
|
|
|
|
iqQuery.appendChild(iqResponse.buildNode('name', {'xmlns': NS_VERSION}, 'Jappix'));
|
|
iqQuery.appendChild(iqResponse.buildNode('version', {'xmlns': NS_VERSION}, JAPPIX_VERSION));
|
|
iqQuery.appendChild(iqResponse.buildNode('os', {'xmlns': NS_VERSION}, BrowserDetect.OS));
|
|
|
|
con.send(iqResponse);
|
|
|
|
logThis('Received software version query: ' + iqFrom);
|
|
}
|
|
|
|
// Last activity query
|
|
else if((iqQueryXMLNS == NS_LAST) && (iqType == 'get')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0012.html */
|
|
|
|
var iqQuery = iqResponse.setQuery(NS_LAST);
|
|
iqQuery.setAttribute('seconds', getLastActivity());
|
|
|
|
con.send(iqResponse);
|
|
|
|
logThis('Received last activity query: ' + iqFrom);
|
|
}
|
|
|
|
// Privacy lists push
|
|
else if((iqQueryXMLNS == NS_PRIVACY) && (iqType == 'set')) {
|
|
// REF : http://xmpp.org/extensions/xep-0016.html
|
|
|
|
// Roster push
|
|
con.send(iqResponse);
|
|
|
|
// Get the lists
|
|
$(iqQuery).find('list').each(function() {
|
|
getPrivacy($(this).attr('name'));
|
|
});
|
|
|
|
logThis('Received privacy lists push: ' + iqFrom);
|
|
}
|
|
|
|
// Roster push
|
|
else if((iqQueryXMLNS == NS_ROSTER) && (iqType == 'set')) {
|
|
// REF : http://xmpp.org/extensions/xep-0092.html
|
|
|
|
// Roster push
|
|
con.send(iqResponse);
|
|
|
|
// Get the values
|
|
$(iqQuery).find('item').each(function() {
|
|
parseRoster($(this), 'presence');
|
|
});
|
|
|
|
logThis('Received roster push: ' + iqFrom);
|
|
}
|
|
|
|
// Roster Item Exchange query
|
|
else if($(iqNode).find('x[xmlns="' + NS_ROSTERX + '"]').size()) {
|
|
// Open a new notification
|
|
newNotification('rosterx', iqFrom, [iqNode], '');
|
|
|
|
logThis('Roster Item Exchange from: ' + iqFrom);
|
|
}
|
|
|
|
// Disco info query
|
|
else if((iqQueryXMLNS == NS_DISCO_INFO) && (iqType == 'get')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0030.html */
|
|
|
|
var iqQuery = iqResponse.setQuery(NS_DISCO_INFO);
|
|
|
|
// We set the name of the client
|
|
iqQuery.appendChild(iqResponse.buildNode('identity', {
|
|
'category': 'client',
|
|
'type': 'web',
|
|
'name': 'Jappix',
|
|
'xmlns': NS_DISCO_INFO
|
|
}));
|
|
|
|
// We set all the supported features
|
|
var fArray = myDiscoInfos();
|
|
|
|
for(i in fArray)
|
|
iqQuery.appendChild(iqResponse.buildNode('feature', {'var': fArray[i], 'xmlns': NS_DISCO_INFO}));
|
|
|
|
con.send(iqResponse);
|
|
|
|
logThis('Received disco#infos query: ' + iqFrom);
|
|
}
|
|
|
|
// User time query
|
|
else if($(iqNode).find('time').size() && (iqType == 'get')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0202.html */
|
|
|
|
var iqTime = iqResponse.appendNode('time', {'xmlns': NS_URN_TIME});
|
|
iqTime.appendChild(iqResponse.buildNode('tzo', {'xmlns': NS_URN_TIME}, getDateTZO()));
|
|
iqTime.appendChild(iqResponse.buildNode('utc', {'xmlns': NS_URN_TIME}, getXMPPTime('utc')));
|
|
|
|
con.send(iqResponse);
|
|
|
|
logThis('Received local time query: ' + iqFrom);
|
|
}
|
|
|
|
// Ping
|
|
else if($(iqNode).find('ping').size() && (iqType == 'get')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0199.html */
|
|
|
|
con.send(iqResponse);
|
|
|
|
logThis('Received a ping: ' + iqFrom);
|
|
}
|
|
|
|
// Not implemented
|
|
else if(!$(iqNode).find('error').size() && ((iqType == 'get') || (iqType == 'set'))) {
|
|
// Change IQ type
|
|
iqResponse.setType('error');
|
|
|
|
// Append stanza content
|
|
for(var i = 0; i < iqNode.childNodes.length; i++)
|
|
iqResponse.getNode().appendChild(iqNode.childNodes.item(i).cloneNode(true));
|
|
|
|
// Append error content
|
|
var iqError = iqResponse.appendNode('error', {'xmlns': NS_CLIENT, 'code': '501', 'type': 'cancel'});
|
|
iqError.appendChild(iqResponse.buildNode('feature-not-implemented', {'xmlns': NS_STANZAS}));
|
|
iqError.appendChild(iqResponse.buildNode('text', {'xmlns': NS_STANZAS}, _e("The feature requested is not implemented by the recipient or server and therefore cannot be processed.")));
|
|
|
|
con.send(iqResponse);
|
|
|
|
logThis('Received an unsupported IQ query from: ' + iqFrom);
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the messages JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 04/05/12
|
|
|
|
*/
|
|
|
|
// Handles the incoming message packets
|
|
function handleMessage(message) {
|
|
// Error packet? Stop!
|
|
if(handleErrorReply(message))
|
|
return;
|
|
|
|
// MAM-forwarded message?
|
|
var c_mam = message.getChild('result', NS_URN_MAM);
|
|
|
|
if(c_mam) {
|
|
var c_mam_delay = $(c_mam).find('delay[xmlns="' + NS_URN_DELAY + '"]');
|
|
var c_mam_forward = $(c_mam).find('forwarded[xmlns="' + NS_URN_FORWARD + '"]');
|
|
|
|
if(c_mam_forward.size()) {
|
|
handleMessageMAM(c_mam_forward, c_mam_delay);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// We get the message items
|
|
var from = fullXID(getStanzaFrom(message));
|
|
var id = message.getID();
|
|
var type = message.getType();
|
|
var body = trim(message.getBody());
|
|
var node = message.getNode();
|
|
var subject = trim(message.getSubject());
|
|
|
|
// Keep raw message body
|
|
var raw_body = body;
|
|
|
|
// We generate some values
|
|
var xid = bareXID(from);
|
|
var resource = thisResource(from);
|
|
var hash = hex_md5(xid);
|
|
var xHTML = $(node).find('html body').size();
|
|
var GCUser = false;
|
|
|
|
// This message comes from a groupchat user
|
|
if(isPrivate(xid) && ((type == 'chat') || !type) && resource) {
|
|
GCUser = true;
|
|
xid = from;
|
|
hash = hex_md5(xid);
|
|
}
|
|
|
|
// Get message date
|
|
var time, stamp;
|
|
var delay = readMessageDelay(node);
|
|
|
|
// Any delay?
|
|
if(delay) {
|
|
time = relativeDate(delay);
|
|
stamp = extractStamp(Date.jab2date(delay));
|
|
} else {
|
|
time = getCompleteTime();
|
|
stamp = extractStamp(new Date());
|
|
}
|
|
|
|
// Received message
|
|
if(hasReceived(message))
|
|
return messageReceived(hash, id);
|
|
|
|
// Chatstate message
|
|
if(node && !delay && ((((type == 'chat') || !type) && !exists('#page-switch .' + hash + ' .unavailable')) || (type == 'groupchat'))) {
|
|
/* REF: http://xmpp.org/extensions/xep-0085.html */
|
|
|
|
// Re-process the hash
|
|
var chatstate_hash = hash;
|
|
|
|
if(type == 'groupchat')
|
|
chatstate_hash = hex_md5(from);
|
|
|
|
// Do something depending of the received state
|
|
if($(node).find('active').size()) {
|
|
displayChatState('active', chatstate_hash, type);
|
|
|
|
// Tell Jappix the entity supports chatstates
|
|
$('#' + chatstate_hash + ' .message-area').attr('data-chatstates', 'true');
|
|
|
|
logThis('Active chatstate received from: ' + from);
|
|
}
|
|
|
|
else if($(node).find('composing').size()) {
|
|
displayChatState('composing', chatstate_hash, type);
|
|
|
|
logThis('Composing chatstate received from: ' + from);
|
|
}
|
|
|
|
else if($(node).find('paused').size()) {
|
|
displayChatState('paused', chatstate_hash, type);
|
|
|
|
logThis('Paused chatstate received from: ' + from);
|
|
}
|
|
|
|
else if($(node).find('inactive').size()){
|
|
displayChatState('inactive', chatstate_hash, type);
|
|
|
|
logThis('Inactive chatstate received from: ' + from);
|
|
}
|
|
|
|
else if($(node).find('gone').size()){
|
|
displayChatState('gone', chatstate_hash, type);
|
|
|
|
logThis('Gone chatstate received from: ' + from);
|
|
}
|
|
}
|
|
|
|
// Jappix App message
|
|
if(message.getChild('app', 'jappix:app')) {
|
|
// Get notification data
|
|
var jappix_app_node = $(node).find('app[xmlns="jappix:app"]');
|
|
var jappix_app_name = jappix_app_node.find('name');
|
|
|
|
var jappix_app_name_id = jappix_app_name.attr('id');
|
|
var jappix_app_name_value = jappix_app_name.text();
|
|
|
|
// Jappix Me notification?
|
|
if(jappix_app_name_id == 'me') {
|
|
// Get more notification data
|
|
var jappix_app_data = jappix_app_node.find('data[xmlns="jappix:app:me"]');
|
|
var jappix_app_data_action = jappix_app_data.find('action');
|
|
var jappix_app_data_url = jappix_app_data.find('url');
|
|
|
|
var jappix_app_data_action_type = jappix_app_data_action.attr('type');
|
|
var jappix_app_data_action_success = jappix_app_data_action.attr('success');
|
|
var jappix_app_data_action_job = jappix_app_data_action.attr('job');
|
|
var jappix_app_data_url_value = jappix_app_data_url.text();
|
|
|
|
// Validate data
|
|
if(jappix_app_data_action_type && jappix_app_data_action_success && jappix_app_data_action_job) {
|
|
// Filter success
|
|
jappix_app_data_action_success = parseInt(jappix_app_data_action_success) == 1 ? 'success' : 'error';
|
|
|
|
// Generate notification namespace
|
|
var jappix_me_notification_ns = jappix_app_name_id + '_' + jappix_app_data_action_type + '_' + jappix_app_data_action_job + '_' + jappix_app_data_action_success;
|
|
|
|
// Open a new notification
|
|
newNotification(jappix_me_notification_ns, xid, [jappix_app_name_value, jappix_app_data_url_value], body);
|
|
|
|
logThis('Jappix Me notification from: ' + xid + ' with namespace: ' + jappix_me_notification_ns);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Invite message
|
|
if($(node).find('x[xmlns="' + NS_MUC_USER + '"] invite').size()) {
|
|
// We get the needed values
|
|
var iFrom = $(node).find('x[xmlns="' + NS_MUC_USER + '"] invite').attr('from');
|
|
var iRoom = $(node).find('x[xmlns="' + NS_XCONFERENCE + '"]').attr('jid');
|
|
|
|
// Old invite method?
|
|
if(!iRoom)
|
|
iRoom = from;
|
|
|
|
// We display the notification
|
|
newNotification('invite_room', iFrom, [iRoom], body);
|
|
|
|
logThis('Invite Request from: ' + iFrom + ' to join: ' + iRoom);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Request message
|
|
if(message.getChild('confirm', NS_HTTP_AUTH)) {
|
|
// Open a new notification
|
|
newNotification('request', xid, [message], body);
|
|
|
|
logThis('HTTP Request from: ' + xid);
|
|
|
|
return false;
|
|
}
|
|
|
|
// OOB message
|
|
if(message.getChild('x', NS_XOOB)) {
|
|
handleOOB(from, id, 'x', node);
|
|
|
|
logThis('Message OOB request from: ' + xid);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Roster Item Exchange message
|
|
if(message.getChild('x', NS_ROSTERX)) {
|
|
// Open a new notification
|
|
newNotification('rosterx', xid, [message], body);
|
|
|
|
logThis('Roster Item Exchange from: ' + xid);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Normal message
|
|
if((type == 'normal') && body) {
|
|
// Message date
|
|
var messageDate = delay;
|
|
|
|
// No message date?
|
|
if(!messageDate)
|
|
messageDate = getXMPPTime('utc');
|
|
|
|
// Message ID
|
|
var messageID = hex_md5(xid + subject + messageDate);
|
|
|
|
// We store the received message
|
|
storeInboxMessage(xid, subject, body, 'unread', messageID, messageDate);
|
|
|
|
// Display the inbox message
|
|
if(exists('#inbox'))
|
|
displayInboxMessage(xid, subject, body, 'unread', messageID, messageDate);
|
|
|
|
// Check we have new messages (play a sound if any unread messages)
|
|
if(checkInboxMessages())
|
|
soundPlay(2);
|
|
|
|
// Send it to the server
|
|
storeInbox();
|
|
|
|
return false;
|
|
}
|
|
|
|
// PubSub event
|
|
if($(node).find('event').attr('xmlns') == NS_PUBSUB_EVENT) {
|
|
// We get the needed values
|
|
var iParse = $(node).find('event items');
|
|
var iNode = iParse.attr('node');
|
|
|
|
// Turn around the different result cases
|
|
if(iNode) {
|
|
switch(iNode) {
|
|
// Mood
|
|
case NS_MOOD:
|
|
// Retrieve the values
|
|
var iMood = iParse.find('mood');
|
|
var fValue = '';
|
|
var tText = '';
|
|
|
|
// There's something
|
|
if(iMood.children().size()) {
|
|
// Read the value
|
|
fValue = node.getElementsByTagName('mood').item(0).childNodes.item(0).nodeName;
|
|
|
|
// Read the text
|
|
tText = iMood.find('text').text();
|
|
|
|
// Avoid errors
|
|
if(!fValue)
|
|
fValue = '';
|
|
}
|
|
|
|
// Store the PEP event (and display it)
|
|
storePEP(xid, 'mood', fValue, tText);
|
|
|
|
break;
|
|
|
|
// Activity
|
|
case NS_ACTIVITY:
|
|
// Retrieve the values
|
|
var iActivity = iParse.find('activity');
|
|
var sValue = '';
|
|
var tText = '';
|
|
|
|
// There's something
|
|
if(iActivity.children().size()) {
|
|
// Read the value
|
|
fValue = node.getElementsByTagName('activity').item(0).childNodes.item(0).nodeName;
|
|
|
|
// Read the text
|
|
tText = iActivity.find('text').text();
|
|
|
|
// Avoid errors
|
|
if(!fValue)
|
|
fValue = '';
|
|
}
|
|
|
|
// Store the PEP event (and display it)
|
|
storePEP(xid, 'activity', fValue, tText);
|
|
|
|
break;
|
|
|
|
// Tune
|
|
case NS_TUNE:
|
|
// Retrieve the values
|
|
var iTune = iParse.find('tune');
|
|
var tArtist = iTune.find('artist').text();
|
|
var tSource = iTune.find('source').text();
|
|
var tTitle = iTune.find('title').text();
|
|
var tURI = iTune.find('uri').text();
|
|
|
|
// Store the PEP event (and display it)
|
|
storePEP(xid, 'tune', tArtist, tTitle, tSource, tURI);
|
|
|
|
break;
|
|
|
|
// Geolocation
|
|
case NS_GEOLOC:
|
|
// Retrieve the values
|
|
var iGeoloc = iParse.find('geoloc');
|
|
var tLat = iGeoloc.find('lat').text();
|
|
var tLon = iGeoloc.find('lon').text();
|
|
|
|
// Any extra-values?
|
|
var tLocality = iGeoloc.find('locality').text();
|
|
var tRegion = iGeoloc.find('region').text();
|
|
var tCountry = iGeoloc.find('country').text();
|
|
var tHuman = humanPosition(tLocality, tRegion, tCountry);
|
|
|
|
// Store the PEP event (and display it)
|
|
storePEP(xid, 'geoloc', tLat, tLon, tHuman);
|
|
|
|
break;
|
|
|
|
// Microblog
|
|
case NS_URN_MBLOG:
|
|
displayMicroblog(message, xid, hash, 'mixed', 'push');
|
|
|
|
break;
|
|
|
|
// Inbox
|
|
case NS_URN_INBOX:
|
|
// Do not handle friend's notifications
|
|
if(xid == getXID())
|
|
handleNotifications(message);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// If this is a room topic message
|
|
if(subject && (type == 'groupchat')) {
|
|
// Filter the vars
|
|
var filter_subject = subject.replace(/\n+/g, ' ');
|
|
var filteredSubject = filterThisMessage(filter_subject, resource, true);
|
|
var filteredName = resource.htmlEnc();
|
|
|
|
// Display the new subject at the top
|
|
$('#' + hash + ' .top .name .bc-infos .muc-topic').replaceWith('<span class="muc-topic" title="' + filter_subject + '">' + filteredSubject + '</span>');
|
|
|
|
// Display the new subject as a system message
|
|
if(resource) {
|
|
var topic_body = filteredName + ' ' + _e("changed the subject to:") + ' ' + filterThisMessage(subject, resource, true);
|
|
displayMessage(type, from, hash, filteredName, topic_body, time, stamp, 'system-message', false);
|
|
}
|
|
}
|
|
|
|
// If the message has a content
|
|
if(xHTML || body) {
|
|
var filteredMessage;
|
|
var html_escape = true;
|
|
|
|
// IE bug fix
|
|
if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9))
|
|
xHTML = 0;
|
|
|
|
//If this is a xHTML message
|
|
if(xHTML) {
|
|
html_escape = false;
|
|
|
|
// Filter the xHTML message
|
|
body = filterThisXHTML(node);
|
|
}
|
|
|
|
// Groupchat message
|
|
if(type == 'groupchat') {
|
|
/* REF: http://xmpp.org/extensions/xep-0045.html */
|
|
|
|
// We generate the message type and time
|
|
var message_type = 'user-message';
|
|
|
|
// This is an old message
|
|
if(delay && resource)
|
|
message_type = 'old-message';
|
|
|
|
// This is a system message
|
|
else if(!resource)
|
|
message_type = 'system-message';
|
|
|
|
var nickQuote = '';
|
|
|
|
// If this is not an old message
|
|
if(message_type == 'user-message') {
|
|
var myNick = getMUCNick(hash);
|
|
|
|
// If an user quoted our nick (with some checks)
|
|
var regex = new RegExp('((^)|( )|(@))' + escapeRegex(myNick) + '(($)|(:)|(,)|( ))', 'gi');
|
|
|
|
if(body.match(regex) && (myNick != resource) && (message_type == 'user-message'))
|
|
nickQuote = ' my-nick';
|
|
|
|
// We notify the user if there's a new personal message
|
|
if(nickQuote) {
|
|
messageNotify(hash, 'personal');
|
|
quickBoard(from, 'groupchat', raw_body, resource);
|
|
soundPlay(1);
|
|
}
|
|
|
|
// We notify the user there's a new unread MUC message
|
|
else {
|
|
messageNotify(hash, 'unread');
|
|
|
|
// Play sound to all users in the MUC, except user who sent the message.
|
|
if(myNick != resource)
|
|
soundPlay(1);
|
|
}
|
|
}
|
|
|
|
// Display the received message
|
|
displayMessage(type, from, hash, resource.htmlEnc(), body, time, stamp, message_type, html_escape, nickQuote);
|
|
}
|
|
|
|
// Chat message
|
|
else {
|
|
// Gets the nickname of the user
|
|
var fromName = resource;
|
|
var chatType = 'chat';
|
|
|
|
// Must send a receipt notification?
|
|
if(hasReceipt(message) && (id != null))
|
|
sendReceived(type, from, id);
|
|
|
|
// It does not come from a groupchat user, get the full name
|
|
if(!GCUser)
|
|
fromName = getBuddyName(xid);
|
|
else
|
|
chatType = 'private';
|
|
|
|
// If the chat isn't yet opened, open it !
|
|
if(!exists('#' + hash)) {
|
|
// We create a new chat
|
|
chatCreate(hash, xid, fromName, chatType);
|
|
|
|
// We tell the user that a new chat has started
|
|
soundPlay(0);
|
|
} else {
|
|
soundPlay(1);
|
|
}
|
|
|
|
// Display the received message
|
|
displayMessage(type, xid, hash, fromName.htmlEnc(), body, time, stamp, 'user-message', html_escape, '', 'him');
|
|
|
|
// We notify the user
|
|
messageNotify(hash, 'personal');
|
|
quickBoard(xid, 'chat', raw_body, fromName);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Sends a given message
|
|
function sendMessage(hash, type) {
|
|
// Get the values
|
|
var message_area = $('#' + hash + ' .message-area');
|
|
var body = trim(message_area.val());
|
|
var xid = unescape(message_area.attr('data-to'));
|
|
|
|
// If the user didn't entered any message, stop
|
|
if(!body || !xid)
|
|
return false;
|
|
|
|
try {
|
|
// We send the message through the XMPP network
|
|
var aMsg = new JSJaCMessage();
|
|
aMsg.setTo(xid);
|
|
|
|
// Set an ID
|
|
var id = genID();
|
|
aMsg.setID(id);
|
|
|
|
// /help shortcut
|
|
if(body.match(/^\/help\s*(.*)/)) {
|
|
// Help text
|
|
var help_text = '<p class="help" xmlns="http://www.w3.org/1999/xhtml">';
|
|
help_text += '<b>' + _e("Available shortcuts:") + '</b>';
|
|
|
|
// Shortcuts array
|
|
var shortcuts = [];
|
|
|
|
// Common shortcuts
|
|
shortcuts.push(printf(_e("%s removes the chat logs"), '<em>/clear</em>'));
|
|
shortcuts.push(printf(_e("%s joins a groupchat"), '<em>/join jid</em>'));
|
|
shortcuts.push(printf(_e("%s closes the chat"), '<em>/part</em>'));
|
|
shortcuts.push(printf(_e("%s shows the user profile"), '<em>/whois jid</em>'));
|
|
|
|
// Groupchat shortcuts
|
|
if(type == 'groupchat') {
|
|
shortcuts.push(printf(_e("%s sends a message to the room"), '<em>/say message</em>'));
|
|
shortcuts.push(printf(_e("%s changes your nickname"), '<em>/nick nickname</em>'));
|
|
shortcuts.push(printf(_e("%s sends a message to someone in the room"), '<em>/msg nickname message</em>'));
|
|
shortcuts.push(printf(_e("%s changes the room topic"), '<em>/topic subject</em>'));
|
|
shortcuts.push(printf(_e("%s kicks a user of the room"), '<em>/kick [reason:] nickname</em>'));
|
|
shortcuts.push(printf(_e("%s bans a user of the room"), '<em>/ban [reason:] nickname</em>'));
|
|
shortcuts.push(printf(_e("%s invites someone to join the room"), '<em>/invite jid message</em>'));
|
|
}
|
|
|
|
// Generate the code from the array
|
|
shortcuts = shortcuts.sort();
|
|
|
|
for(s in shortcuts)
|
|
help_text += shortcuts[s] + '<br />';
|
|
|
|
help_text += '</p>';
|
|
|
|
// Display the message
|
|
displayMessage(type, xid, hash, 'help', help_text, getCompleteTime(), getTimeStamp(), 'system-message', false);
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// /clear shortcut
|
|
else if(body.match(/^\/clear/)) {
|
|
cleanChat(hex_md5(xid));
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// /join shortcut
|
|
else if(body.match(/^\/join (\S+)\s*(.*)/)) {
|
|
// Join
|
|
var room = generateXID(RegExp.$1, 'groupchat');
|
|
var pass = RegExp.$2;
|
|
|
|
checkChatCreate(room, 'groupchat');
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// /part shortcut
|
|
else if(body.match(/^\/part\s*(.*)/) && (!isAnonymous() || (isAnonymous() && (xid != generateXID(ANONYMOUS_ROOM, 'groupchat')))))
|
|
quitThisChat(xid, hex_md5(xid), type);
|
|
|
|
// /whois shortcut
|
|
else if(body.match(/^\/whois(( (\S+))|($))/)) {
|
|
var whois_xid = RegExp.$3;
|
|
|
|
// Groupchat WHOIS
|
|
if(type == 'groupchat') {
|
|
var nXID = getMUCUserXID(xid, whois_xid);
|
|
|
|
if(!nXID)
|
|
openThisInfo(6);
|
|
else
|
|
openUserInfos(nXID);
|
|
}
|
|
|
|
// Chat or private WHOIS
|
|
else {
|
|
if(!whois_xid)
|
|
openUserInfos(xid);
|
|
else
|
|
openUserInfos(whois_xid);
|
|
}
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// Chat message type
|
|
else if(type == 'chat') {
|
|
aMsg.setType('chat');
|
|
|
|
// Generates the correct message depending of the choosen style
|
|
var genMsg = generateMessage(aMsg, body, hash);
|
|
var html_escape = genMsg != 'XHTML';
|
|
|
|
// Receipt request
|
|
var receipt_request = receiptRequest(hash);
|
|
|
|
if(receipt_request)
|
|
aMsg.appendNode('request', {'xmlns': NS_URN_RECEIPTS});
|
|
|
|
// Chatstate
|
|
aMsg.appendNode('active', {'xmlns': NS_CHATSTATES});
|
|
|
|
// Send it!
|
|
con.send(aMsg, handleErrorReply);
|
|
|
|
// Filter the xHTML message (for us!)
|
|
if(!html_escape)
|
|
body = filterThisXHTML(aMsg.getNode());
|
|
|
|
// Finally we display the message we just sent
|
|
var my_xid = getXID();
|
|
|
|
displayMessage('chat', my_xid, hash, getBuddyName(my_xid).htmlEnc(), body, getCompleteTime(), getTimeStamp(), 'user-message', html_escape, '', 'me', id);
|
|
|
|
// Receipt timer
|
|
if(receipt_request)
|
|
checkReceived(hash, id);
|
|
}
|
|
|
|
// Groupchat message type
|
|
else if(type == 'groupchat') {
|
|
// /say shortcut
|
|
if(body.match(/^\/say (.+)/)) {
|
|
body = body.replace(/^\/say (.+)/, '$1');
|
|
|
|
aMsg.setType('groupchat');
|
|
generateMessage(aMsg, body, hash);
|
|
|
|
con.send(aMsg, handleErrorReply);
|
|
}
|
|
|
|
// /nick shortcut
|
|
else if(body.match(/^\/nick (.+)/)) {
|
|
var nick = body.replace(/^\/nick (.+)/, '$1');
|
|
|
|
// Does not exist yet?
|
|
if(!getMUCUserXID(xid, nick)) {
|
|
// Send a new presence
|
|
sendPresence(xid + '/' + nick, '', getUserShow(), getUserStatus(), '', false, false, handleErrorReply);
|
|
|
|
// Change the stored nickname
|
|
$('#' + hex_md5(xid)).attr('data-nick', escape(nick));
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
}
|
|
|
|
// /msg shortcut
|
|
else if(body.match(/^\/msg (\S+)\s+(.+)/)) {
|
|
var nick = RegExp.$1;
|
|
var body = RegExp.$2;
|
|
var nXID = getMUCUserXID(xid, nick);
|
|
|
|
// We check if the user exists
|
|
if(!nXID)
|
|
openThisInfo(6);
|
|
|
|
// If the private message is not empty
|
|
else if(body) {
|
|
aMsg.setType('chat');
|
|
aMsg.setTo(nXID);
|
|
generateMessage(aMsg, body, hash);
|
|
|
|
con.send(aMsg, handleErrorReply);
|
|
}
|
|
}
|
|
|
|
// /topic shortcut
|
|
else if(body.match(/^\/topic (.+)/)) {
|
|
var topic = body.replace(/^\/topic (.+)/, '$1');
|
|
|
|
aMsg.setType('groupchat');
|
|
aMsg.setSubject(topic);
|
|
|
|
con.send(aMsg, handleMessageError);
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// /ban shortcut
|
|
else if(body.match(/^\/ban (.*)/)) {
|
|
nick = jQuery.trim(RegExp.$1);
|
|
reason='';
|
|
var nXID = getMUCUserRealXID(xid, nick);
|
|
// We check if the user exists, if not it may be because a reason is given
|
|
// we do not check it at first because the nickname could contain ':'
|
|
if(!nXID && (body.match(/^\/ban ([^:]+)[:]*(.*)/))) {
|
|
var reason = jQuery.trim(RegExp.$1);
|
|
var nick = jQuery.trim(RegExp.$2);
|
|
if (0==nick.length) {
|
|
nick = reason;
|
|
reason='';
|
|
}
|
|
var nXID = getMUCUserXID(xid, nick);
|
|
}
|
|
|
|
// We check if the user exists
|
|
if(!nXID)
|
|
openThisInfo(6);
|
|
|
|
else {
|
|
// We generate the ban IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setTo(xid);
|
|
iq.setType('set');
|
|
|
|
var iqQuery = iq.setQuery(NS_MUC_ADMIN);
|
|
var item = iqQuery.appendChild(iq.buildNode('item', {'affiliation': 'outcast', 'jid': nXID, 'xmlns': NS_MUC_ADMIN}));
|
|
|
|
if(reason)
|
|
item.appendChild(iq.buildNode('reason', {'xmlns': NS_MUC_ADMIN}, reason));
|
|
|
|
con.send(iq, handleErrorReply);
|
|
}
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// /kick shortcut
|
|
else if(body.match(/^\/kick (.*)/)) {
|
|
nick = jQuery.trim(RegExp.$1);
|
|
reason='';
|
|
var nXID = getMUCUserRealXID(xid, nick);
|
|
// We check if the user exists, if not it may be because a reason is given
|
|
// we do not check it at first because the nickname could contain ':'
|
|
if(!nXID && (body.match(/^\/kick ([^:]+)[:]*(.*)/))) {
|
|
var reason = jQuery.trim(RegExp.$1);
|
|
var nick = jQuery.trim(RegExp.$2);
|
|
if (0==nick.length) {
|
|
nick = reason;
|
|
reason='';
|
|
}
|
|
var nXID = getMUCUserXID(xid, nick);
|
|
}
|
|
|
|
// We check if the user exists
|
|
if(!nXID)
|
|
openThisInfo(6);
|
|
|
|
else {
|
|
// We generate the kick IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setTo(xid);
|
|
iq.setType('set');
|
|
|
|
var iqQuery = iq.setQuery(NS_MUC_ADMIN);
|
|
var item = iqQuery.appendChild(iq.buildNode('item', {'nick': nick, 'role': 'none', 'xmlns': NS_MUC_ADMIN}));
|
|
|
|
if(reason)
|
|
item.appendChild(iq.buildNode('reason', {'xmlns': NS_MUC_ADMIN}, reason));
|
|
|
|
con.send(iq, handleErrorReply);
|
|
}
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// /invite shortcut
|
|
else if(body.match(/^\/invite (\S+)\s*(.*)/)) {
|
|
var i_xid = RegExp.$1;
|
|
var reason = RegExp.$2;
|
|
|
|
var x = aMsg.appendNode('x', {'xmlns': NS_MUC_USER});
|
|
var aNode = x.appendChild(aMsg.buildNode('invite', {'to': i_xid, 'xmlns': NS_MUC_USER}));
|
|
|
|
if(reason)
|
|
aNode.appendChild(aMsg.buildNode('reason', {'xmlns': NS_MUC_USER}, reason));
|
|
|
|
con.send(aMsg, handleErrorReply);
|
|
|
|
// Reset chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
|
|
// No shortcut, this is a message
|
|
else {
|
|
aMsg.setType('groupchat');
|
|
|
|
// Chatstate
|
|
aMsg.appendNode('active', {'xmlns': NS_CHATSTATES});
|
|
|
|
generateMessage(aMsg, body, hash);
|
|
|
|
con.send(aMsg, handleMessageError);
|
|
|
|
logThis('Message sent to: ' + xid + ' / ' + type, 3);
|
|
}
|
|
}
|
|
|
|
// We reset the message input
|
|
$('#' + hash + ' .message-area').val('');
|
|
}
|
|
|
|
finally {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Generates the correct message area style
|
|
function generateStyle(hash) {
|
|
// Initialize the vars
|
|
var styles = '#' + hash + ' div.bubble-style';
|
|
var font = styles + ' a.font-current';
|
|
var fontsize = styles + ' a.fontsize-current';
|
|
var checkbox = styles + ' input[type="checkbox"]';
|
|
var color = '#' + hash + ' .message-area[data-color]';
|
|
var style = '';
|
|
|
|
// Get the font value
|
|
$(font).filter('[data-font]').each(function() {
|
|
style += 'font-family: ' + $(this).attr('data-font') + ';';
|
|
});
|
|
|
|
// Get the font value
|
|
$(fontsize).filter('[data-value]').each(function() {
|
|
style += 'font-size: ' + $(this).attr('data-value') + 'px;';
|
|
});
|
|
|
|
// Loop the input values
|
|
$(checkbox).filter(':checked').each(function() {
|
|
// If there is a previous element
|
|
if(style)
|
|
style += ' ';
|
|
|
|
// Get the current style
|
|
switch($(this).attr('class')) {
|
|
case 'bold':
|
|
style += 'font-weight: bold;';
|
|
break;
|
|
|
|
case 'italic':
|
|
style += 'font-style: italic;';
|
|
break;
|
|
|
|
case 'underline':
|
|
style += 'text-decoration: underline;';
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Get the color value
|
|
$(color).each(function() {
|
|
style += 'color: #' + $(this).attr('data-color');
|
|
});
|
|
|
|
return style;
|
|
}
|
|
|
|
// Generates the correct message code
|
|
function generateMessage(aMsg, body, hash) {
|
|
// Create the classical body
|
|
aMsg.setBody(body);
|
|
|
|
// Get the style
|
|
var style = $('#' + hash + ' .message-area').attr('style');
|
|
|
|
// A message style is choosen
|
|
if(style) {
|
|
// Explode the message body new lines (to create one <p /> element by line)
|
|
var new_lines = new Array(body);
|
|
|
|
if(body.match(/\n/))
|
|
new_lines = body.split('\n');
|
|
|
|
// Create the XML elements
|
|
var aHtml = aMsg.appendNode('html', {'xmlns': NS_XHTML_IM});
|
|
var aBody = aHtml.appendChild(aMsg.buildNode('body', {'xmlns': NS_XHTML}));
|
|
|
|
// Use the exploded body array to create one element per entry
|
|
for(i in new_lines) {
|
|
// Current line
|
|
var cLine = new_lines[i];
|
|
|
|
// Blank line, we put a <br />
|
|
if(cLine.match(/(^)(\s+)($)/) || !cLine)
|
|
aBody.appendChild(aMsg.buildNode('br', {'xmlns': NS_XHTML}));
|
|
|
|
// Line with content, we put a <p />
|
|
else {
|
|
// HTML encode the line
|
|
cLine = cLine.htmlEnc();
|
|
|
|
// Filter the links
|
|
cLine = applyLinks(cLine, 'xhtml-im', style);
|
|
|
|
// Append the filtered line
|
|
$(aBody).append($('<p style="' + style + '">' + cLine + '</p>'));
|
|
}
|
|
}
|
|
|
|
return 'XHTML';
|
|
}
|
|
|
|
return 'PLAIN';
|
|
}
|
|
|
|
// Displays a given message in a chat tab
|
|
function displayMessage(type, xid, hash, name, body, time, stamp, message_type, html_escape, nick_quote, mode, id, c_target_sel, no_scroll) {
|
|
// Target
|
|
if(typeof c_target_sel === 'undefined') {
|
|
c_target_sel = $('#' + hash + ' .content');
|
|
}
|
|
|
|
// Generate some stuffs
|
|
var has_avatar = false;
|
|
var xid_hash = '';
|
|
|
|
if(!nick_quote) {
|
|
nick_quote = '';
|
|
}
|
|
|
|
if(message_type != 'system-message') {
|
|
has_avatar = true;
|
|
xid_hash = hex_md5(xid);
|
|
}
|
|
|
|
// Can scroll?
|
|
var cont_scroll = document.getElementById('chat-content-' + hash);
|
|
var can_scroll = false;
|
|
|
|
if((!cont_scroll.scrollTop || ((cont_scroll.clientHeight + cont_scroll.scrollTop) == cont_scroll.scrollHeight)) && no_scroll != true) {
|
|
can_scroll = true;
|
|
}
|
|
|
|
// Any ID?
|
|
var data_id = '';
|
|
|
|
if(id) {
|
|
data_id = ' data-id="' + id + '"';
|
|
}
|
|
|
|
// Filter the message
|
|
var filteredMessage = filterThisMessage(body, name, html_escape);
|
|
|
|
// Display the received message in the room
|
|
var messageCode = '<div class="one-line ' + message_type + nick_quote + '" data-stamp="' + stamp + '"' + data_id + '>';
|
|
|
|
// Name color attribute
|
|
if(type == 'groupchat') {
|
|
attribute = ' style="color: ' + generateColor(name) + ';" class="name';
|
|
} else {
|
|
attribute = ' class="name';
|
|
|
|
if(mode) {
|
|
attribute += ' ' + mode;
|
|
}
|
|
}
|
|
|
|
// Close the class attribute
|
|
if(message_type == 'system-message') {
|
|
attribute += ' hidden"';
|
|
} else {
|
|
attribute += '"';
|
|
}
|
|
|
|
// Filter the previous displayed message
|
|
var last = c_target_sel.find('.one-group:not(.from-history):last');
|
|
var last_name = last.find('b.name').attr('data-xid');
|
|
var last_type = last.attr('data-type');
|
|
var last_stamp = parseInt(last.attr('data-stamp'));
|
|
var grouped = false;
|
|
|
|
// We can group it with another previous message
|
|
if((last_name == xid) && (message_type == last_type) && ((stamp - last_stamp) <= 1800)) {
|
|
grouped = true;
|
|
}
|
|
|
|
// Is it a /me command?
|
|
if(body.match(/(^|>)(\/me )([^<]+)/)) {
|
|
filteredMessage = '<i>' + filteredMessage + '</i>';
|
|
}
|
|
|
|
messageCode += filteredMessage + '</div>';
|
|
|
|
// Must group it?
|
|
if(!grouped) {
|
|
// Generate message headers
|
|
var message_head = '';
|
|
|
|
// Any avatar to add?
|
|
if(has_avatar) {
|
|
message_head += '<div class="avatar-container"><img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" /></div>';
|
|
}
|
|
|
|
// Add the date & the name
|
|
message_head += '<span class="date">' + time + '</span><b data-xid="' + encodeQuotes(xid) + '" ' + attribute + '>' + name + '</b>';
|
|
|
|
// Generate message code
|
|
messageCode = '<div class="one-group ' + xid_hash + '" data-type="' + message_type + '" data-stamp="' + stamp + '">' + message_head + messageCode + '</div>';
|
|
}
|
|
|
|
// Write the code in the DOM
|
|
if(grouped) {
|
|
c_target_sel.find('.one-group:last').append(messageCode);
|
|
} else {
|
|
c_target_sel.append(messageCode);
|
|
}
|
|
|
|
// Store the last MAM_REQ_MAX message groups
|
|
if(!enabledMAM() && (type == 'chat') && (message_type == 'user-message')) {
|
|
// Filter the DOM
|
|
var dom_filter = $('#' + hash + ' .content').clone().contents();
|
|
var default_avatar = ('./img/others/default-avatar.png').replace(/&/g, '&'); // Fixes #252
|
|
|
|
$(dom_filter).find('.system-message').parent().remove();
|
|
$(dom_filter).find('.avatar-container img.avatar').attr('src', default_avatar);
|
|
$(dom_filter).find('.one-line').parent().slice(0, $(dom_filter).find('.one-line').parent().size() - MAM_REQ_MAX).remove();
|
|
|
|
var store_html = $(dom_filter).parent().html();
|
|
|
|
// Store the data
|
|
if(store_html) {
|
|
setPersistent(getXID(), 'history', hash, store_html);
|
|
}
|
|
}
|
|
|
|
// Must get the avatar?
|
|
if(has_avatar && xid && !grouped) {
|
|
getAvatar(xid, 'cache', 'true', 'forget');
|
|
}
|
|
|
|
// Scroll to this message
|
|
if(can_scroll) {
|
|
autoScroll(hash);
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the chatstate JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 25/08/11
|
|
|
|
*/
|
|
|
|
// Sends a given chatstate to a given entity
|
|
function chatStateSend(state, xid, hash) {
|
|
var user_type = $('#' + hash).attr('data-type');
|
|
|
|
// If the friend client supports chatstates and is online
|
|
if((user_type == 'groupchat') || ((user_type == 'chat') && $('#' + hash + ' .message-area').attr('data-chatstates') && !exists('#page-switch .' + hash + ' .unavailable'))) {
|
|
// Already sent?
|
|
if(getDB('currentchatstate', xid) == state)
|
|
return;
|
|
|
|
// Write the state
|
|
setDB('currentchatstate', xid, state);
|
|
|
|
// New message stanza
|
|
var aMsg = new JSJaCMessage();
|
|
aMsg.setTo(xid);
|
|
aMsg.setType(user_type);
|
|
|
|
// Append the chatstate node
|
|
aMsg.appendNode(state, {'xmlns': NS_CHATSTATES});
|
|
|
|
// Send this!
|
|
con.send(aMsg);
|
|
}
|
|
}
|
|
|
|
// Displays a given chatstate in a given chat
|
|
function displayChatState(state, hash, type) {
|
|
// Groupchat?
|
|
if(type == 'groupchat') {
|
|
resetChatState(hash, type);
|
|
|
|
// "gone" state not allowed
|
|
if(state != 'gone')
|
|
$('#page-engine .page-engine-chan .user.' + hash).addClass(state);
|
|
}
|
|
|
|
// Chat
|
|
else {
|
|
// We change the buddy name color in the page-switch
|
|
resetChatState(hash, type);
|
|
$('#page-switch .' + hash + ' .name').addClass(state);
|
|
|
|
// We generate the chatstate text
|
|
var text = '';
|
|
|
|
switch(state) {
|
|
// Active
|
|
case 'active':
|
|
text = _e("Your friend is paying attention to the conversation.");
|
|
|
|
break;
|
|
|
|
// Composing
|
|
case 'composing':
|
|
text = _e("Your friend is writing a message...");
|
|
|
|
break;
|
|
|
|
// Paused
|
|
case 'paused':
|
|
text = _e("Your friend stopped writing a message.");
|
|
|
|
break;
|
|
|
|
// Inactive
|
|
case 'inactive':
|
|
text = _e("Your friend is doing something else.");
|
|
|
|
break;
|
|
|
|
// Gone
|
|
case 'gone':
|
|
text = _e("Your friend closed the chat.");
|
|
|
|
break;
|
|
}
|
|
|
|
// We reset the previous state
|
|
$('#' + hash + ' .chatstate').remove();
|
|
|
|
// We create the chatstate
|
|
$('#' + hash + ' .content').after('<div class="' + state + ' chatstate">' + text + '</div>');
|
|
}
|
|
}
|
|
|
|
// Resets the chatstate switcher marker
|
|
function resetChatState(hash, type) {
|
|
// Define the selector
|
|
var selector;
|
|
|
|
if(type == 'groupchat')
|
|
selector = $('#page-engine .page-engine-chan .user.' + hash);
|
|
else
|
|
selector = $('#page-switch .' + hash + ' .name');
|
|
|
|
// Reset!
|
|
selector.removeClass('active')
|
|
selector.removeClass('composing')
|
|
selector.removeClass('paused')
|
|
selector.removeClass('inactive')
|
|
selector.removeClass('gone');
|
|
}
|
|
|
|
// Adds the chatstate events
|
|
function eventsChatState(target, xid, hash, type) {
|
|
target.keyup(function(e) {
|
|
if(e.keyCode != 13) {
|
|
// Composing a message
|
|
if($(this).val() && (getDB('chatstate', xid) != 'on')) {
|
|
// We change the state detect input
|
|
setDB('chatstate', xid, 'on');
|
|
|
|
// We send the friend a "composing" chatstate
|
|
chatStateSend('composing', xid, hash);
|
|
}
|
|
|
|
// Flushed the message which was being composed
|
|
else if(!$(this).val() && (getDB('chatstate', xid) == 'on')) {
|
|
// We change the state detect input
|
|
setDB('chatstate', xid, 'off');
|
|
|
|
// We send the friend an "active" chatstate
|
|
chatStateSend('active', xid, hash);
|
|
}
|
|
}
|
|
});
|
|
|
|
target.change(function() {
|
|
// Reset the composing database entry
|
|
setDB('chatstate', xid, 'off');
|
|
});
|
|
|
|
target.focus(function() {
|
|
// Not needed
|
|
if(target.is(':disabled'))
|
|
return;
|
|
|
|
// Something was written, user started writing again
|
|
if($(this).val())
|
|
chatStateSend('composing', xid, hash);
|
|
|
|
// Chat only: Nothing in the input, user is active
|
|
else if(type == 'chat')
|
|
chatStateSend('active', xid, hash);
|
|
});
|
|
|
|
target.blur(function() {
|
|
// Not needed
|
|
if(target.is(':disabled'))
|
|
return;
|
|
|
|
// Something was written, user paused
|
|
if($(this).val())
|
|
chatStateSend('paused', xid, hash);
|
|
|
|
// Chat only: Nothing in the input, user is inactive
|
|
else if(type == 'chat')
|
|
chatStateSend('inactive', xid, hash);
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the receipts JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 26/12/10
|
|
|
|
*/
|
|
|
|
// Checks if we can send a receipt request
|
|
function receiptRequest(hash) {
|
|
// Entity have support for receipt?
|
|
if($('#' + hash + ' .message-area').attr('data-receipts') == 'true')
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if there is a receipt request
|
|
function hasReceipt(packet) {
|
|
// Any receipt request?
|
|
if(packet.getChild('request', NS_URN_RECEIPTS))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if there is a received reply
|
|
function hasReceived(packet) {
|
|
// Any received reply?
|
|
if(packet.getChild('received', NS_URN_RECEIPTS))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Sends a received notification
|
|
function sendReceived(type, to, id) {
|
|
var aMsg = new JSJaCMessage();
|
|
aMsg.setTo(to);
|
|
aMsg.setID(id);
|
|
|
|
// Any type?
|
|
if(type)
|
|
aMsg.setType(type);
|
|
|
|
// Append the received node
|
|
aMsg.appendNode('received', {'xmlns': NS_URN_RECEIPTS, 'id': id});
|
|
|
|
con.send(aMsg);
|
|
|
|
logThis('Sent received to: ' + to);
|
|
}
|
|
|
|
// Tells the message has been received
|
|
function messageReceived(hash, id) {
|
|
// Line selector
|
|
var path = $('#' + hash + ' .one-line[data-id="' + id + '"]');
|
|
|
|
// Add a received marker
|
|
path.attr('data-received', 'true')
|
|
.removeAttr('data-lost');
|
|
|
|
// Group selector
|
|
var group = path.parent();
|
|
|
|
// Remove the group marker
|
|
if(!group.find('.one-line[data-lost]').size()) {
|
|
group.find('b.name').removeClass('talk-images')
|
|
.removeAttr('title');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if the message has been received
|
|
function checkReceived(hash, id) {
|
|
// Fire a check 10 seconds later
|
|
$('#' + hash + ' .one-line[data-id="' + id + '"]').oneTime('10s', function() {
|
|
// Not received?
|
|
if($(this).attr('data-received') != 'true') {
|
|
// Add a "lost" marker
|
|
$(this).attr('data-lost', 'true');
|
|
|
|
// Add a warn on the buddy-name
|
|
$(this).parent().find('b.name').addClass('talk-images')
|
|
.attr('title', _e("Your friend seems not to have received your message(s)!"));
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the tooltip JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 10/04/12
|
|
|
|
*/
|
|
|
|
// Creates a tooltip code
|
|
function createTooltip(xid, hash, type) {
|
|
// Path to the element
|
|
var path = '#' + hash;
|
|
var path_tooltip = path + ' .chat-tools-' + type;
|
|
var path_bubble = path_tooltip + ' .bubble-' + type;
|
|
|
|
// Yet exists?
|
|
if(exists(path_bubble))
|
|
return false;
|
|
|
|
// Generates special tooltip HTML code
|
|
var title = '';
|
|
var content = '';
|
|
|
|
switch(type) {
|
|
// Smileys
|
|
case 'smileys':
|
|
title = _e("Smiley insertion");
|
|
content = smileyLinks(hash);
|
|
|
|
break;
|
|
|
|
// Style
|
|
case 'style':
|
|
title = _e("Change style");
|
|
|
|
// Generate fonts list
|
|
var fonts = {
|
|
'arial': 'Arial, Helvetica, sans-serif',
|
|
'arial-black': '\'Arial Black\', Gadget, sans-serif',
|
|
'bookman-old-style': '\'Bookman Old Style\', serif',
|
|
'comic-sans-ms': '\'Comic Sans MS\', cursive',
|
|
'courier': 'Courier, monospace',
|
|
'courier-new': '\'Courier New\', Courier, monospace',
|
|
'garamond': 'Garamond, serif',
|
|
'georgia': 'Georgia, serif',
|
|
'impact': 'Impact, Charcoal, sans-serif',
|
|
'lucida-console': '\'Lucida Console\', Monaco, monospace',
|
|
'lucida-sans-unicode': '\'Lucida Sans Unicode\', \'Lucida Grande\', sans-serif',
|
|
'ms-sans-serif': '\'MS Sans Serif\', Geneva, sans-serif',
|
|
'ms-serif': '\'MS Serif\', \'New York\', sans-serif',
|
|
'palatino-linotype': '\'Palatino Linotype\', \'Book Antiqua\', Palatino, serif',
|
|
'symbol': 'Symbol, sans-serif',
|
|
'tahoma': 'Tahoma, Geneva, sans-serif',
|
|
'times-new-roman': '\'Times New Roman\', Times, serif',
|
|
'trebuchet-ms': '\'Trebuchet MS\', Helvetica, sans-serif',
|
|
'verdana': 'Verdana, Geneva, sans-serif',
|
|
'webdings': 'Webdings, sans-serif',
|
|
'wingdings': 'Wingdings, \'Zapf Dingbats\', sans-serif'
|
|
};
|
|
|
|
var fonts_html = '<div class="font-list">';
|
|
|
|
// No fonts
|
|
fonts_html += '<a href="#">' + _e("None") + '</a>';
|
|
|
|
// Available fonts
|
|
$.each(fonts, function(id_name, full_name) {
|
|
// Generate short name
|
|
var short_name = full_name;
|
|
|
|
if(short_name.match(/,/)) {
|
|
var name_split = short_name.split(',');
|
|
short_name = trim(name_split[0]);
|
|
}
|
|
|
|
short_name = short_name.replace(/([^a-z0-9\s]+)/gi, '');
|
|
|
|
// Add this to the HTML
|
|
fonts_html += '<a href="#" data-value="' + encodeQuotes(id_name) + '" data-font="' + encodeQuotes(full_name) + '" style="font-family: ' + encodeQuotes(full_name) + ';">' + short_name.htmlEnc() + '</a>';
|
|
});
|
|
fonts_html += '</div>';
|
|
|
|
content =
|
|
'<label class="font">' +
|
|
'<div class="font-icon talk-images"></div>' +
|
|
|
|
'<div class="fontsize-change">' +
|
|
'<a class="fontsize-current" href="#">12</a>' +
|
|
'<div class="fontsize-list">' +
|
|
'<a href="#" class="reset talk-images"></a>' +
|
|
'<a href="#" data-value="10" style="font-size: 10px;">10</a>' +
|
|
'<a href="#" data-value="12" style="font-size: 12px;">12</a>' +
|
|
'<a href="#" data-value="14" style="font-size: 14px;">14</a>' +
|
|
'<a href="#" data-value="16" style="font-size: 16px;">16</a>' +
|
|
'<a href="#" data-value="18" style="font-size: 18px;">18</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="font-change">' +
|
|
'<a class="font-current" href="#">' + _e("None") + '</a>' +
|
|
fonts_html +
|
|
'</div>' +
|
|
'</label>' +
|
|
'<label class="bold"><input type="checkbox" class="bold" />' + _e("Text in bold") + '</label>' +
|
|
'<label class="italic"><input type="checkbox" class="italic" />' + _e("Text in italic") + '</label>' +
|
|
'<label class="underline"><input type="checkbox" class="underline" />' + _e("Underlined text") + '</label>' +
|
|
'<a href="#" class="color" style="background-color: #b10808; clear: both;" data-color="b10808"></a>' +
|
|
'<a href="#" class="color" style="background-color: #e5860c;" data-color="e5860c"></a>' +
|
|
'<a href="#" class="color" style="background-color: #f0f30e;" data-color="f0f30e"></a>' +
|
|
'<a href="#" class="color" style="background-color: #009a04;" data-color="009a04"></a>' +
|
|
'<a href="#" class="color" style="background-color: #0ba9a0;" data-color="0ba9a0"></a>' +
|
|
'<a href="#" class="color" style="background-color: #04228f;" data-color="04228f"></a>' +
|
|
'<a href="#" class="color" style="background-color: #9d0ab7;" data-color="9d0ab7"></a>' +
|
|
'<div class="color-picker">' +
|
|
'<a href="#" class="color-more talk-images"></a>' +
|
|
'<div class="color-hex">' +
|
|
'<span class="hex-begin">#</span>' +
|
|
'<input class="hex-value" type="text" maxlength="6" placeholder="e1e1e1" />' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
break;
|
|
|
|
// File send
|
|
case 'file':
|
|
title = _e("Send a file");
|
|
content = '<p style="margin-bottom: 8px;">' + _e("Once uploaded, your friend will be prompted to download the file you sent.") + '</p>';
|
|
content += '<form id="oob-upload" action="./php/send.php" method="post" enctype="multipart/form-data">' + generateFileShare() + '</form>';
|
|
|
|
break;
|
|
|
|
// Chat log
|
|
case 'save':
|
|
title = _e("Save chat");
|
|
content = '<p style="margin-bottom: 8px;">' + _e("Click on the following link to get the chat log, and wait. Then click again to get the file.") + '</p>';
|
|
|
|
// Possible to generate any log?
|
|
if($(path + ' .one-line').size())
|
|
content += '<a href="#" class="tooltip-actionlog">' + _e("Generate file!") + '</a>';
|
|
else
|
|
content += '<span class="tooltip-nolog">' + _e("This chat is empty!") + '</span>';
|
|
|
|
break;
|
|
}
|
|
|
|
// Generates general tooltip HTML code
|
|
var html =
|
|
'<div class="tooltip bubble-' + type + '">' +
|
|
'<div class="tooltip-subitem">' +
|
|
'<p class="tooltip-top">' + title + '</p>' +
|
|
content +
|
|
'</div>' +
|
|
|
|
'<div class="tooltip-subarrow talk-images"></div>' +
|
|
'</div>';
|
|
|
|
// Append the HTML code
|
|
$(path_tooltip).append(html);
|
|
|
|
// Special events
|
|
switch(type) {
|
|
// Smileys
|
|
case 'smileys':
|
|
// Apply click event on smiley links
|
|
$(path_tooltip + ' a.emoticon').click(function() {
|
|
return insertSmiley($(this).attr('data-smiley'), hash);
|
|
});
|
|
|
|
break;
|
|
|
|
// Style
|
|
case 'style':
|
|
// Paths to items
|
|
var message_area = path + ' .message-area';
|
|
var bubble_style = path_tooltip + ' .bubble-style';
|
|
var style = bubble_style + ' input:checkbox';
|
|
var colors = bubble_style + ' a.color';
|
|
var font_current = bubble_style + ' a.font-current';
|
|
var font_list = bubble_style + ' div.font-list';
|
|
var font_select = font_list + ' a';
|
|
var fontsize_current = bubble_style + ' a.fontsize-current';
|
|
var fontsize_list = bubble_style + ' div.fontsize-list';
|
|
var fontsize_select = fontsize_list + ' a';
|
|
var color = bubble_style + ' div.color-picker';
|
|
var color_more = color + ' a.color-more';
|
|
var color_hex = color + ' div.color-hex';
|
|
|
|
// Click event on style bubble
|
|
$(bubble_style).click(function() {
|
|
// Hide font selector if opened
|
|
if($(font_list).is(':visible'))
|
|
$(font_current).click();
|
|
|
|
// Hide font-size selector if opened
|
|
if($(fontsize_list).is(':visible'))
|
|
$(fontsize_current).click();
|
|
|
|
// Hide color selector if opened
|
|
if($(color_hex).is(':visible'))
|
|
$(color_more).click();
|
|
});
|
|
|
|
// Click event on font picker
|
|
$(font_current).click(function() {
|
|
// The clicked color is yet selected
|
|
if($(font_list).is(':visible'))
|
|
$(this).parent().removeClass('listed');
|
|
else
|
|
$(this).parent().addClass('listed');
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event on a new font in the picker
|
|
$(font_select).click(function() {
|
|
// No font selected
|
|
if(!$(this).attr('data-value')) {
|
|
$(font_current).removeAttr('data-font')
|
|
.removeAttr('data-value')
|
|
.text(_e("None"));
|
|
|
|
$(message_area).removeAttr('data-font');
|
|
}
|
|
|
|
// A font is defined
|
|
else {
|
|
$(font_current).attr('data-font', $(this).attr('data-font'))
|
|
.attr('data-value', $(this).attr('data-value'))
|
|
.text($(font_list).find('a[data-value="' + $(this).attr('data-value') + '"]').text());
|
|
|
|
$(message_area).attr('data-font', $(this).attr('data-value'));
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event on font-size picker
|
|
$(fontsize_current).click(function() {
|
|
// The clicked color is yet selected
|
|
if($(fontsize_list).is(':visible'))
|
|
$(this).parent().removeClass('listed');
|
|
else
|
|
$(this).parent().addClass('listed');
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event on a new font-size in the picker
|
|
$(fontsize_select).click(function() {
|
|
// No font-size selected
|
|
if(!$(this).attr('data-value')) {
|
|
$(fontsize_current).removeAttr('data-value').text(_e("16"));
|
|
$(message_area).removeAttr('data-fontsize');
|
|
}
|
|
|
|
// A font-size is defined
|
|
else {
|
|
$(fontsize_current).attr('data-value', $(this).attr('data-value'))
|
|
.text($(this).attr('data-value'));
|
|
$(message_area).attr('data-fontsize', $(this).attr('data-value'));
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event on color picker
|
|
$(colors).click(function() {
|
|
// Reset the manual picker
|
|
$(color_hex).find('input').val('');
|
|
|
|
// The clicked color is yet selected
|
|
if($(this).hasClass('selected')) {
|
|
$(message_area).removeAttr('data-color');
|
|
$(this).removeClass('selected');
|
|
}
|
|
|
|
else {
|
|
$(message_area).attr('data-color', $(this).attr('data-color'));
|
|
$(colors).removeClass('selected');
|
|
$(this).addClass('selected');
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event on color picker
|
|
$(color_more).click(function() {
|
|
// The clicked color is yet selected
|
|
if($(color_hex).is(':visible'))
|
|
$(this).parent().removeClass('opened');
|
|
|
|
else {
|
|
$(this).parent().addClass('opened');
|
|
|
|
// Focus
|
|
$(document).oneTime(10, function() {
|
|
$(color_hex).find('input').focus();
|
|
});
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event on color hex
|
|
$(color_hex).click(function() {
|
|
return false;
|
|
});
|
|
|
|
// Keyup event on color picker
|
|
$(color_hex).find('input').keyup(function(e) {
|
|
// Submit
|
|
if(e.keyCode == 13) {
|
|
if($(color_hex).is(':visible')) {
|
|
$(color_more).click();
|
|
|
|
// Focus again on the message textarea
|
|
$(document).oneTime(10, function() {
|
|
$(message_area).focus();
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Reset current color
|
|
$(message_area).removeAttr('data-color');
|
|
$(colors).removeClass('selected');
|
|
|
|
// Change value
|
|
var new_value = $(this).val().replace(/([^a-z0-9]+)/gi, '');
|
|
$(this).val(new_value);
|
|
|
|
if(new_value)
|
|
$(message_area).attr('data-color', new_value);
|
|
|
|
// Regenerate style
|
|
var style = generateStyle(hash);
|
|
|
|
// Any style to apply?
|
|
if(style)
|
|
$(message_area).attr('style', style);
|
|
else
|
|
$(message_area).removeAttr('style');
|
|
}).placeholder();
|
|
|
|
// Change event on text style checkboxes
|
|
$(style).change(function() {
|
|
// Get current type
|
|
var style_data = 'data-' + $(this).attr('class');
|
|
|
|
// Checked checkbox?
|
|
if($(this).filter(':checked').size())
|
|
$(message_area).attr(style_data, true);
|
|
else
|
|
$(message_area).removeAttr(style_data);
|
|
});
|
|
|
|
// Update the textarea style when it is changed
|
|
$(style + ', ' + colors + ', ' + font_select + ', ' + fontsize_select).click(function() {
|
|
var style = generateStyle(hash);
|
|
|
|
// Any style to apply?
|
|
if(style)
|
|
$(message_area).attr('style', style);
|
|
else
|
|
$(message_area).removeAttr('style');
|
|
|
|
// Focus again on the message textarea
|
|
$(document).oneTime(10, function() {
|
|
$(message_area).focus();
|
|
});
|
|
});
|
|
|
|
// Load current style
|
|
loadStyleSelector(hash);
|
|
|
|
break;
|
|
|
|
// File send
|
|
case 'file':
|
|
// File upload vars
|
|
var oob_upload_options = {
|
|
dataType: 'xml',
|
|
beforeSubmit: waitUploadOOB,
|
|
success: handleUploadOOB
|
|
};
|
|
|
|
// Upload form submit event
|
|
$(path_tooltip + ' #oob-upload').submit(function() {
|
|
if($(path_tooltip + ' #oob-upload input[type="file"]').val())
|
|
$(this).ajaxSubmit(oob_upload_options);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Upload input change event
|
|
$(path_tooltip + ' #oob-upload input[type="file"]').change(function() {
|
|
if($(this).val())
|
|
$(path_tooltip + ' #oob-upload').ajaxSubmit(oob_upload_options);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Input click event
|
|
$(path_tooltip + ' #oob-upload input[type="file"], ' + path_tooltip + ' #oob-upload input[type="submit"]').click(function() {
|
|
if(exists(path_tooltip + ' #oob-upload input[type="reset"]'))
|
|
return;
|
|
|
|
// Lock the bubble
|
|
$(path_bubble).addClass('locked');
|
|
|
|
// Add a cancel button
|
|
$(this).after('<input type="reset" value="' + _e("Cancel") + '" />');
|
|
|
|
// Cancel button click event
|
|
$(path_tooltip + ' #oob-upload input[type="reset"]').click(function() {
|
|
// Remove the bubble
|
|
$(path_bubble).removeClass('locked');
|
|
destroyTooltip(hash, 'file');
|
|
});
|
|
});
|
|
|
|
break;
|
|
|
|
// Chat log
|
|
case 'save':
|
|
// Chat log generation click event
|
|
$(path_tooltip + ' .tooltip-actionlog').click(function() {
|
|
// Replace it with a waiting notice
|
|
$(this).replaceWith('<span class="tooltip-waitlog">' + _e("Please wait...") + '</span>');
|
|
|
|
generateChatLog(xid, hash);
|
|
|
|
return false;
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Destroys a tooltip code
|
|
function destroyTooltip(hash, type) {
|
|
$('#' + hash + ' .chat-tools-content:not(.mini) .bubble-' + type + ':not(.locked)').remove();
|
|
}
|
|
|
|
// Applies the page-engine tooltips hover event
|
|
function hoverTooltip(xid, hash, type) {
|
|
$('#' + hash + ' .chat-tools-' + type).hover(function() {
|
|
createTooltip(xid, hash, type);
|
|
}, function() {
|
|
destroyTooltip(hash, type)
|
|
});
|
|
}
|
|
|
|
// Applies the hoverTooltip function to the needed things
|
|
function tooltipIcons(xid, hash) {
|
|
// Hover events
|
|
hoverTooltip(xid, hash, 'smileys');
|
|
hoverTooltip(xid, hash, 'style');
|
|
hoverTooltip(xid, hash, 'file');
|
|
hoverTooltip(xid, hash, 'save');
|
|
|
|
// Click events
|
|
$('#' + hash + ' a.chat-tools-content, #' + hash + ' .chat-tools-content a').click(function() {
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Loads the style selector options
|
|
function loadStyleSelector(hash) {
|
|
// Define the vars
|
|
var path = '#' + hash;
|
|
var message_area = $(path + ' .message-area');
|
|
var bubble_style = path + ' .bubble-style';
|
|
var font = message_area.attr('data-font');
|
|
var font_select = $(bubble_style + ' div.font-list').find('a[data-value="' + font + '"]');
|
|
var fontsize = message_area.attr('data-fontsize');
|
|
var color = message_area.attr('data-color');
|
|
|
|
// Apply message font
|
|
if(font) {
|
|
$(bubble_style + ' a.font-current').attr('data-value', font)
|
|
.attr('data-font', font_select.attr('data-font'))
|
|
.text(font_select.text());
|
|
}
|
|
|
|
// Apply message font-size
|
|
if(fontsize) {
|
|
$(bubble_style + ' a.fontsize-current').attr('data-value', fontsize)
|
|
.text(fontsize);
|
|
}
|
|
|
|
// Apply the options to the style selector
|
|
$(bubble_style + ' input[type="checkbox"]').each(function() {
|
|
// Current input enabled?
|
|
if(message_area.attr('data-' + $(this).attr('class')))
|
|
$(this).attr('checked', true);
|
|
});
|
|
|
|
// Apply message color
|
|
if(color) {
|
|
if($(bubble_style + ' a.color[data-color="' + color + '"]').size())
|
|
$(bubble_style + ' a.color[data-color="' + color + '"]').addClass('selected');
|
|
else
|
|
$(bubble_style + ' div.color-hex input.hex-value').val(color);
|
|
}
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the filtering JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 13/02/12
|
|
|
|
*/
|
|
|
|
// Generates a given emoticon HTML code
|
|
function emoteImage(image, text, after) {
|
|
return ' <img class="emoticon emoticon-' + image + ' smileys-images" alt="' + encodeQuotes(text) + '" src="' + './img/others/blank.gif' + '" /> ' + after;
|
|
}
|
|
|
|
// Filters a given message
|
|
function filterThisMessage(neutralMessage, nick, html_escape) {
|
|
var filteredMessage = neutralMessage;
|
|
|
|
// We encode the HTML special chars
|
|
if(html_escape)
|
|
filteredMessage = filteredMessage.htmlEnc();
|
|
|
|
// /me command
|
|
filteredMessage = filteredMessage.replace(/((^)|((.+)(>)))(\/me )([^<]+)/, nick + ' $7')
|
|
|
|
// We replace the smilies text into images
|
|
.replace(/(:-?@)($|\s|<)/gi, emoteImage('angry', '$1', '$2'))
|
|
.replace(/(:-?\[)($|\s|<)/gi, emoteImage('bat', '$1', '$2'))
|
|
.replace(/(\(B\))($|\s|<)/g, emoteImage('beer', '$1', '$2'))
|
|
.replace(/((:-?D)|(XD))($|\s|<)/gi, emoteImage('biggrin', '$1', '$4'))
|
|
.replace(/(:-?\$)($|\s|<)/gi, emoteImage('blush', '$1', '$2'))
|
|
.replace(/(\(Z\))($|\s|<)/g, emoteImage('boy', '$1', '$2'))
|
|
.replace(/(\(W\))($|\s|<)/g, emoteImage('brflower', '$1', '$2'))
|
|
.replace(/((<\/3)|(\(U\)))($|\s|<)/g, emoteImage('brheart', '$1', '$4'))
|
|
.replace(/(\(C\))($|\s|<)/g, emoteImage('coffee', '$1', '$2'))
|
|
.replace(/((8-\))|(\(H\)))($|\s|<)/g, emoteImage('coolglasses', '$1', '$4'))
|
|
.replace(/(:'-?\()($|\s|<)/gi, emoteImage('cry', '$1', '$2'))
|
|
.replace(/(\(%\))($|\s|<)/g, emoteImage('cuffs', '$1', '$2'))
|
|
.replace(/(\]:-?>)($|\s|<)/gi, emoteImage('devil', '$1', '$2'))
|
|
.replace(/(\(D\))($|\s|<)/g, emoteImage('drink', '$1', '$2'))
|
|
.replace(/(@}->--)($|\s|<)/gi, emoteImage('flower', '$1', '$2'))
|
|
.replace(/((:-?\/)|(:-?S))($|\s|<)/gi, emoteImage('frowning', '$1', '$4'))
|
|
.replace(/(\(X\))($|\s|<)/g, emoteImage('girl', '$1', '$2'))
|
|
.replace(/((<3)|(\(L\)))($|\s|<)/g, emoteImage('heart', '$1', '$4'))
|
|
.replace(/(\(}\))($|\s|<)/g, emoteImage('hugleft', '$1', '$2'))
|
|
.replace(/(\({\))($|\s|<)/g, emoteImage('hugright', '$1', '$2'))
|
|
.replace(/(:-?{})($|\s|<)/gi, emoteImage('kiss', '$1', '$2'))
|
|
.replace(/(\(I\))($|\s|<)/g, emoteImage('lamp', '$1', '$2'))
|
|
.replace(/(:-?3)($|\s|<)/gi, emoteImage('lion', '$1', '$2'))
|
|
.replace(/(\(E\))($|\s|<)/g, emoteImage('mail', '$1', '$2'))
|
|
.replace(/(\(S\))($|\s|<)/g, emoteImage('moon', '$1', '$2'))
|
|
.replace(/(\(8\))($|\s|<)/g, emoteImage('music', '$1', '$2'))
|
|
.replace(/((=-?O)|(:-?O))($|\s|<)/gi, emoteImage('oh', '$1', '$4'))
|
|
.replace(/(\(T\))($|\s|<)/g, emoteImage('phone', '$1', '$2'))
|
|
.replace(/(\(P\))($|\s|<)/g, emoteImage('photo', '$1', '$2'))
|
|
.replace(/(:-?!)($|\s|<)/gi, emoteImage('puke', '$1', '$2'))
|
|
.replace(/(\(@\))($|\s|<)/g, emoteImage('pussy', '$1', '$2'))
|
|
.replace(/(\(R\))($|\s|<)/g, emoteImage('rainbow', '$1', '$2'))
|
|
.replace(/(:-?\))($|\s|<)/gi, emoteImage('smile', '$1', '$2'))
|
|
.replace(/(\(\*\))($|\s|<)/g, emoteImage('star', '$1', '$2'))
|
|
.replace(/(:-?\|)($|\s|<)/gi, emoteImage('stare', '$1', '$2'))
|
|
.replace(/(\(N\))($|\s|<)/g, emoteImage('thumbdown', '$1', '$2'))
|
|
.replace(/(\(Y\))($|\s|<)/g, emoteImage('thumbup', '$1', '$2'))
|
|
.replace(/(:-?P)($|\s|<)/gi, emoteImage('tongue', '$1', '$2'))
|
|
.replace(/(:-?\()($|\s|<)/gi, emoteImage('unhappy', '$1', '$2'))
|
|
.replace(/(;-?\))($|\s|<)/gi, emoteImage('wink', '$1', '$2'))
|
|
|
|
// Text in bold
|
|
.replace(/(^|\s|>|\()((\*)([^<>'"\*]+)(\*))($|\s|<|\))/gi, '$1<b>$2</b>$6')
|
|
|
|
// Italic text
|
|
.replace(/(^|\s|>|\()((\/)([^<>'"\/]+)(\/))($|\s|<|\))/gi, '$1<em>$2</em>$6')
|
|
|
|
// Underlined text
|
|
.replace(/(^|\s|>|\()((_)([^<>'"_]+)(_))($|\s|<|\))/gi, '$1<span style="text-decoration: underline;">$2</span>$6');
|
|
|
|
// Add the links
|
|
if(html_escape)
|
|
filteredMessage = applyLinks(filteredMessage, 'desktop');
|
|
|
|
// Filter integratebox links
|
|
filteredMessage = filterIntegrateBox(filteredMessage);
|
|
|
|
return filteredMessage;
|
|
}
|
|
|
|
// Filters a xHTML message to be displayed in Jappix
|
|
function filterThisXHTML(code) {
|
|
// Allowed elements array
|
|
var elements = new Array(
|
|
'a',
|
|
'abbr',
|
|
'acronym',
|
|
'address',
|
|
'blockquote',
|
|
'body',
|
|
'br',
|
|
'cite',
|
|
'code',
|
|
'dd',
|
|
'dfn',
|
|
'div',
|
|
'dt',
|
|
'em',
|
|
'h1',
|
|
'h2',
|
|
'h3',
|
|
'h4',
|
|
'h5',
|
|
'h6',
|
|
'head',
|
|
'html',
|
|
'img',
|
|
'kbd',
|
|
'li',
|
|
'ol',
|
|
'p',
|
|
'pre',
|
|
'q',
|
|
'samp',
|
|
'span',
|
|
'strong',
|
|
'title',
|
|
'ul',
|
|
'var'
|
|
);
|
|
|
|
// Allowed attributes array
|
|
var attributes = new Array(
|
|
'accesskey',
|
|
'alt',
|
|
'charset',
|
|
'cite',
|
|
'class',
|
|
'height',
|
|
'href',
|
|
'hreflang',
|
|
'id',
|
|
'longdesc',
|
|
'profile',
|
|
'rel',
|
|
'rev',
|
|
'src',
|
|
'style',
|
|
'tabindex',
|
|
'title',
|
|
'type',
|
|
'uri',
|
|
'version',
|
|
'width',
|
|
'xml:lang',
|
|
'xmlns'
|
|
);
|
|
|
|
// Remove forbidden elements
|
|
$(code).find('html body *').each(function() {
|
|
// This element is not authorized
|
|
if(!existArrayValue(elements, (this).nodeName.toLowerCase()))
|
|
$(this).remove();
|
|
});
|
|
|
|
// Remove forbidden attributes
|
|
$(code).find('html body *').each(function() {
|
|
// Put a pointer on this element (jQuery way & normal way)
|
|
var cSelector = $(this);
|
|
var cElement = (this);
|
|
|
|
// Loop the attributes of the current element
|
|
$(cElement.attributes).each(function(index) {
|
|
// Read the current attribute
|
|
var cAttr = cElement.attributes[index];
|
|
var cName = cAttr.name;
|
|
var cVal = cAttr.value;
|
|
|
|
// This attribute is not authorized, or contains JS code
|
|
if(!existArrayValue(attributes, cName.toLowerCase()) || ((cVal.toLowerCase()).match(/(^|"|')javascript:/)))
|
|
cSelector.removeAttr(cName);
|
|
});
|
|
});
|
|
|
|
// Filter some other elements
|
|
$(code).find('a').attr('target', '_blank');
|
|
|
|
return $(code).find('html body').html();
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the links JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: dual-licensed under AGPL and MPLv2
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 26/08/11
|
|
|
|
*/
|
|
|
|
// Apply links in a string
|
|
function applyLinks(string, mode, style) {
|
|
// Special stuffs
|
|
var style, target;
|
|
|
|
// Links style
|
|
if(!style)
|
|
style = '';
|
|
else
|
|
style = ' style="' + style + '"';
|
|
|
|
// Open in new tabs
|
|
if(mode != 'xhtml-im')
|
|
target = ' target="_blank"';
|
|
else
|
|
target = '';
|
|
|
|
// XMPP address
|
|
string = string.replace(/(\s|<br \/>|^)(([a-zA-Z0-9\._-]+)@([a-zA-Z0-9\.\/_-]+))(,|\s|$)/gi, '$1<a href="xmpp:$2" target="_blank"' + style + '>$2</a>$5');
|
|
|
|
// Simple link
|
|
string = string.replace(/(\s|<br \/>|^|\()((https?|ftp|file|xmpp|irc|mailto|vnc|webcal|ssh|ldap|smb|magnet|spotify)(:)([^<>'"\s\)]+))/gim, '$1<a href="$2"' + target + style + '>$2</a>');
|
|
|
|
return string;
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the inbox JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 08/06/11
|
|
|
|
*/
|
|
|
|
// Opens the inbox popup
|
|
function openInbox() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Your inbox") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="head inbox-head">' +
|
|
'<div class="head-text inbox-head-text">' + _e("Available actions") + '</div>' +
|
|
|
|
'<div class="head-actions inbox-head-actions">' +
|
|
'<a href="#" class="a-delete-messages">' + _e("Clean") + '</a>' +
|
|
'<a href="#" class="a-new-message">' + _e("New") + '</a>' +
|
|
'<a href="#" class="a-show-messages">' + _e("Received") + '</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="inbox-results">' +
|
|
'<p class="inbox-noresults">' + _e("Your inbox is empty.") + '</p>' +
|
|
|
|
'<div class="inbox"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="inbox-new">' +
|
|
'<div class="inbox-new-to inbox-new-block search">' +
|
|
'<p class="inbox-new-text">' + _e("To") + '</p>' +
|
|
|
|
'<input name="inbox-new-to-input" class="inbox-new-input inbox-new-to-input" type="text" required="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="inbox-new-topic inbox-new-block">' +
|
|
'<p class="inbox-new-text">' + _e("Subject") + '</p>' +
|
|
|
|
'<input name="inbox-new-subject-input" class="inbox-new-input inbox-new-subject-input" type="text" required="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="inbox-new-body inbox-new-block">' +
|
|
'<p class="inbox-new-text">' + _e("Content") + '</p>' +
|
|
|
|
'<textarea class="inbox-new-textarea" rows="8" cols="60" required=""></textarea>' +
|
|
'</div>' +
|
|
|
|
'<form class="inbox-new-file inbox-new-block" action="./php/file-share.php" method="post" enctype="multipart/form-data">' +
|
|
'<p class="inbox-new-text">' + _e("File") + '</p>' +
|
|
|
|
generateFileShare() +
|
|
'</form>' +
|
|
|
|
'<div class="inbox-new-send inbox-new-block">' +
|
|
'<a href="#" class="send one-button talk-images">' + _e("Send message") + '</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('inbox', html);
|
|
|
|
// Associate the events
|
|
launchInbox();
|
|
|
|
// Load the messages
|
|
loadInbox();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Closes the inbox popup
|
|
function closeInbox() {
|
|
// Destroy the popup
|
|
destroyPopup('inbox');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Opens the message compose tool
|
|
function composeInboxMessage(xid) {
|
|
// Open things
|
|
openInbox();
|
|
newInboxMessage();
|
|
|
|
// Apply XID
|
|
$('#inbox .inbox-new-to-input').val(xid);
|
|
|
|
// Focus to the next item
|
|
$(document).oneTime(10, function() {
|
|
$('#inbox .inbox-new-subject-input').focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Stores the inbox
|
|
function storeInbox() {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
var query = iq.setQuery(NS_PRIVATE);
|
|
var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_INBOX}));
|
|
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored message
|
|
if(explodeThis('_', current, 0) == 'inbox') {
|
|
// Get the values
|
|
var value = $(XMLFromString(storageDB.getItem(current)));
|
|
|
|
// Create the storage node
|
|
storage.appendChild(iq.buildNode('message', {
|
|
'id': value.find('id').text().revertHtmlEnc(),
|
|
'from': value.find('from').text().revertHtmlEnc(),
|
|
'subject': value.find('subject').text().revertHtmlEnc(),
|
|
'status': value.find('status').text().revertHtmlEnc(),
|
|
'date': value.find('date').text().revertHtmlEnc(),
|
|
'xmlns': NS_INBOX
|
|
},
|
|
|
|
value.find('content').text().revertHtmlEnc()
|
|
));
|
|
}
|
|
}
|
|
|
|
con.send(iq);
|
|
}
|
|
|
|
// Creates a new normal message
|
|
function newInboxMessage() {
|
|
// Init
|
|
var mPath = '#inbox .';
|
|
|
|
// Reset the previous buddy search
|
|
resetBuddySearch('#inbox .inbox-new-to');
|
|
|
|
// We switch the divs
|
|
$(mPath + 'inbox-results, #inbox .a-new-message, #inbox .a-delete-messages').hide();
|
|
$(mPath + 'inbox-new, #inbox .a-show-messages').show();
|
|
|
|
// We focus on the first input
|
|
$(document).oneTime(10, function() {
|
|
$(mPath + 'inbox-new-to-input').focus();
|
|
});
|
|
|
|
// We reset some stuffs
|
|
cleanNewInboxMessage();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Cleans the inbox
|
|
function cleanNewInboxMessage() {
|
|
// Init
|
|
var mPath = '#inbox .';
|
|
|
|
// We reset the forms
|
|
$(mPath + 'inbox-new-block:not(form) input, ' + mPath + 'inbox-new textarea').val('').removeClass('please-complete');
|
|
$(mPath + 'inbox-new-file a').remove();
|
|
$(mPath + 'inbox-new-file input').show();
|
|
|
|
// We close an eventual opened message
|
|
$(mPath + 'message-content').remove();
|
|
$(mPath + 'one-message').removeClass('message-reading');
|
|
}
|
|
|
|
// Sends a normal message
|
|
function sendInboxMessage(to, subject, body) {
|
|
// We send the message
|
|
var mess = new JSJaCMessage();
|
|
|
|
// Main attributes
|
|
mess.setTo(to);
|
|
mess.setSubject(subject);
|
|
mess.setType('normal');
|
|
|
|
// Any file to attach?
|
|
var attached = '#inbox .inbox-new-file a.file';
|
|
|
|
if(exists(attached))
|
|
body += '\n' +
|
|
'\n' +
|
|
$(attached).attr('data-attachedtitle') + ' - ' + $(attached).attr('data-attachedhref');
|
|
|
|
// Set body
|
|
mess.setBody(body);
|
|
|
|
con.send(mess, handleErrorReply);
|
|
}
|
|
|
|
// Performs the normal message sender checks
|
|
function checkInboxMessage() {
|
|
// We get some informations
|
|
var mPath = '#inbox ';
|
|
var to = $(mPath + '.inbox-new-to-input').val();
|
|
var body = $(mPath + '.inbox-new-textarea').val();
|
|
var subject = $(mPath + '.inbox-new-subject-input').val();
|
|
|
|
if(to && body && subject) {
|
|
// New array of XID
|
|
var xid = new Array(to);
|
|
|
|
// More than one XID
|
|
if(to.indexOf(',') != -1)
|
|
xid = to.split(',');
|
|
|
|
for(i in xid) {
|
|
var current = xid[i];
|
|
|
|
// No current value?
|
|
if(!current || current.match(/^(\s+)$/))
|
|
continue;
|
|
|
|
// Edit the XID if needed
|
|
current = current.replace(/ /g, '');
|
|
current = generateXID(current, 'chat');
|
|
|
|
// We send the message
|
|
sendInboxMessage(current, subject, body);
|
|
|
|
// We clean the inputs
|
|
cleanNewInboxMessage();
|
|
|
|
logThis('Inbox message sent: ' + current, 3);
|
|
}
|
|
|
|
// Close the inbox
|
|
closeInbox();
|
|
}
|
|
|
|
else {
|
|
$(mPath + 'input[type="text"], ' + mPath + 'textarea').each(function() {
|
|
var current = this;
|
|
|
|
if(!$(current).val()) {
|
|
$(document).oneTime(10, function() {
|
|
$(current).addClass('please-complete').focus();
|
|
});
|
|
}
|
|
|
|
else
|
|
$(current).removeClass('please-complete');
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Shows the inbox messages
|
|
function showInboxMessages() {
|
|
// Init
|
|
var mPath = '#inbox .';
|
|
|
|
// We switch the divs
|
|
$(mPath + 'inbox-new').hide();
|
|
$(mPath + 'inbox-results').show();
|
|
|
|
// We show a new link in the menu
|
|
$(mPath + 'a-show-messages').hide();
|
|
$(mPath + 'a-delete-messages').show();
|
|
$(mPath + 'a-new-message').show();
|
|
|
|
// We reset some stuffs
|
|
cleanNewInboxMessage();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Displays a normal message
|
|
function displayInboxMessage(from, subject, content, status, id, date) {
|
|
// Generate some paths
|
|
var inbox = '#inbox .';
|
|
var one_message = inbox + 'one-message.' + id;
|
|
|
|
// Message yet displayed!
|
|
if(exists(one_message))
|
|
return false;
|
|
|
|
// Get the nearest element
|
|
var stamp = extractStamp(Date.jab2date(date));
|
|
var nearest = sortElementByStamp(stamp, '#inbox .one-message');
|
|
|
|
// Get the buddy name
|
|
var name = getBuddyName(from).htmlEnc();
|
|
|
|
// We generate the html code
|
|
var nContent = '<div class="one-message message-' + status + ' ' + id + ' ' + hex_md5(from) + '" data-stamp="' + stamp + '">' +
|
|
'<div class="message-head">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="message-jid">' + name + '</div>' +
|
|
'<div class="message-subject">' + subject.htmlEnc() + '</div>' +
|
|
|
|
'<div class="message-truncated">' + truncate(noLines(content), 90).htmlEnc() + '</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// Display the message
|
|
if(nearest == 0)
|
|
$(inbox + 'inbox-results .inbox').append(nContent);
|
|
else
|
|
$('#inbox .one-message[data-stamp="' + nearest + '"]:first').before(nContent);
|
|
|
|
// Click events
|
|
$(one_message + ' .message-head').click(function() {
|
|
if(!exists(one_message + ' .message-content'))
|
|
revealInboxMessage(id, from, subject, content, name, date, status);
|
|
else
|
|
hideInboxMessage(id);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Get the user avatar
|
|
getAvatar(from, 'cache', 'true', 'forget');
|
|
|
|
return true;
|
|
}
|
|
|
|
// Stores an inbox message
|
|
function storeInboxMessage(from, subject, content, status, id, date) {
|
|
// Initialize the XML data
|
|
var xml = '<message><id>' + id.htmlEnc().htmlEnc() + '</id><date>' + date.htmlEnc().htmlEnc() + '</date><from>' + from.htmlEnc().htmlEnc() + '</from><subject>' + subject.htmlEnc().htmlEnc() + '</subject><status>' + status.htmlEnc().htmlEnc() + '</status><content>' + content.htmlEnc().htmlEnc() + '</content>';
|
|
|
|
// End the XML data
|
|
xml += '</message>';
|
|
|
|
// Store this message!
|
|
setDB('inbox', id, xml);
|
|
}
|
|
|
|
// Removes a given normal message
|
|
function deleteInboxMessage(id) {
|
|
// Remove the message from the inbox
|
|
$('#inbox .one-message.' + id).remove();
|
|
|
|
// Remove the message from the database
|
|
removeDB('inbox', id);
|
|
|
|
// Check the unread messages
|
|
checkInboxMessages();
|
|
|
|
// Store the new inbox
|
|
storeInbox();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Removes all the inbox messages
|
|
function purgeInbox() {
|
|
// Remove all the messages from the database
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored message
|
|
if(explodeThis('_', current, 0) == 'inbox')
|
|
removeDB('inbox', explodeThis('_', current, 1));
|
|
}
|
|
|
|
// Prevent the database lag
|
|
$(document).oneTime(100, function() {
|
|
// Store the new inbox
|
|
storeInbox();
|
|
|
|
// Remove all the messages from the inbox
|
|
$('#inbox .one-message').remove();
|
|
|
|
// Reload the inbox
|
|
loadInbox();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if there are new messages to be notified
|
|
function checkInboxMessages() {
|
|
// Selectors
|
|
var inbox_link = '#top-content a.inbox-hidable';
|
|
var no_results = '#inbox .inbox-noresults';
|
|
|
|
// Marker
|
|
var has_messages = false;
|
|
|
|
// Read the number of unread messages
|
|
var unread = 0;
|
|
|
|
// Read the local inbox database
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Database pointer
|
|
var current = storageDB.key(i);
|
|
|
|
// Check inbox messages
|
|
if(explodeThis('_', current, 0) == 'inbox') {
|
|
// Read the current status
|
|
var status = $(XMLFromString(storageDB.getItem(current))).find('status').text();
|
|
|
|
// Found an unread message
|
|
if(status == 'unread')
|
|
unread++;
|
|
|
|
// Update the marker
|
|
has_messages = true;
|
|
}
|
|
}
|
|
|
|
// No message?
|
|
if(!has_messages)
|
|
$(no_results).show();
|
|
else
|
|
$(no_results).hide();
|
|
|
|
// Reset notifications
|
|
$(inbox_link + ' .notify').remove();
|
|
|
|
// Any unread message?
|
|
if(unread) {
|
|
// Notify the user
|
|
$(inbox_link).prepend('<div class="notify one-counter" data-counter="' + unread + '">' + unread + '</div>');
|
|
|
|
// Update the title
|
|
updateTitle();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Anyway, update the title
|
|
updateTitle();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Reveal a normal message content
|
|
function revealInboxMessage(id, from, subject, content, name, date, status) {
|
|
// Message path
|
|
var all_message = '#inbox .one-message';
|
|
var one_message = all_message + '.' + id;
|
|
var one_content = one_message + ' .message-content';
|
|
|
|
// We reset all the other messages
|
|
$(all_message + ' .message-content').remove();
|
|
$(all_message).removeClass('message-reading');
|
|
|
|
// Message content
|
|
var html =
|
|
'<div class="message-content">' +
|
|
'<div class="message-body">' + filterThisMessage(content, name, true) + '</div>' +
|
|
|
|
'<div class="message-meta">' +
|
|
'<span class="date">' + parseDate(date) + '</span>' +
|
|
|
|
'<a href="#" class="reply one-button talk-images">' + _e("Reply") + '</a>' +
|
|
'<a href="#" class="remove one-button talk-images">' + _e("Delete") + '</a>' +
|
|
|
|
'<div class="clear">' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// Message content
|
|
html += '</div>';
|
|
|
|
$(one_message).append(html).addClass('message-reading');
|
|
|
|
// Click events
|
|
$(one_content + ' a.reply').click(function() {
|
|
return replyInboxMessage(id, from, subject, content);
|
|
});
|
|
|
|
$(one_content + ' a.remove').click(function() {
|
|
return deleteInboxMessage(id);
|
|
});
|
|
|
|
// Unread message
|
|
if(status == 'unread') {
|
|
// Update our database
|
|
var xml = getDB('inbox', id).replace(/<status>unread<\/status>/i,'<status>read</status>');
|
|
setDB('inbox', id, xml);
|
|
|
|
// Remove the unread class
|
|
$(one_message).removeClass('message-unread');
|
|
|
|
// Send it to the server!
|
|
storeInbox();
|
|
}
|
|
|
|
// Check the unread messages
|
|
checkInboxMessages();
|
|
}
|
|
|
|
// Hides a normal message content
|
|
function hideInboxMessage(id) {
|
|
// Define the paths
|
|
var inbox = '#inbox .';
|
|
var one_message = inbox + 'one-message.' + id;
|
|
|
|
// Reset this message
|
|
$(one_message).removeClass('message-reading');
|
|
$(one_message + ' .message-content').remove();
|
|
}
|
|
|
|
// Replies to a given normal message
|
|
function replyInboxMessage(id, from, subject, body) {
|
|
// We switch to the writing div
|
|
newInboxMessage();
|
|
|
|
// Inbox path
|
|
var inbox = '#inbox .';
|
|
|
|
// Generate the body
|
|
var body = '\n' + '____________' + '\n\n' + truncate(body, 120);
|
|
|
|
// We apply the generated values to the form
|
|
$(inbox + 'inbox-new-to-input').val(from);
|
|
$(inbox + 'inbox-new-subject-input').val(subject);
|
|
|
|
$(document).oneTime(10, function() {
|
|
$(inbox + 'inbox-new-textarea').val(body).focus().selectRange(1, 0);
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Loads the inbox messages
|
|
function loadInbox() {
|
|
// Read the local database
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored message
|
|
if(explodeThis('_', current, 0) == 'inbox') {
|
|
// Get the current value
|
|
var value = $(XMLFromString(storageDB.getItem(current)));
|
|
|
|
// Display the current message
|
|
displayInboxMessage(
|
|
value.find('from').text().revertHtmlEnc(),
|
|
value.find('subject').text().revertHtmlEnc(),
|
|
value.find('content').text().revertHtmlEnc(),
|
|
value.find('status').text().revertHtmlEnc(),
|
|
value.find('id').text().revertHtmlEnc(),
|
|
value.find('date').text().revertHtmlEnc()
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check new messages
|
|
checkInboxMessages();
|
|
}
|
|
|
|
// Wait event for file attaching
|
|
function waitInboxAttach() {
|
|
$('#inbox .wait').show();
|
|
}
|
|
|
|
// Success event for file attaching
|
|
function handleInboxAttach(responseXML) {
|
|
// Data selector
|
|
var dData = $(responseXML).find('jappix');
|
|
|
|
// Process the returned data
|
|
if(dData.find('error').size()) {
|
|
openThisError(4);
|
|
|
|
logThis('Error while attaching the file: ' + dData.find('error').text(), 1);
|
|
}
|
|
|
|
else {
|
|
// Get the file values
|
|
var fName = dData.find('title').text();
|
|
var fType = dData.find('type').text();
|
|
var fURL = dData.find('href').text();
|
|
|
|
// Hide the attach link, show the unattach one
|
|
$('#inbox .inbox-new-file input').hide();
|
|
$('#inbox .inbox-new-file').append('<a class="file ' + encodeQuotes(fileCategory(explodeThis('/', fType, 1))) + ' talk-images" href="' + encodeQuotes(fURL) + '" target="_blank">' + fName.htmlEnc() + '</a><a href="#" class="remove one-button talk-images">' + _e("Remove") + '</a>');
|
|
|
|
// Set values to the file link
|
|
$('#inbox .inbox-new-file a.file').attr('data-attachedtitle', fName)
|
|
.attr('data-attachedhref', fURL);
|
|
|
|
// Click events
|
|
$('#inbox .inbox-new-file a.remove').click(function() {
|
|
$('#inbox .inbox-new-file a').remove();
|
|
$('#inbox .inbox-new-file input').show();
|
|
|
|
return false;
|
|
});
|
|
|
|
logThis('File attached.', 3);
|
|
}
|
|
|
|
// Reset the attach bubble
|
|
$('#inbox .inbox-new-file input[type="file"]').val('');
|
|
$('#inbox .wait').hide();
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchInbox() {
|
|
// Define the pats
|
|
var inbox = '#inbox .';
|
|
|
|
// Define the buddy search vars
|
|
var destination = inbox + 'inbox-new-to';
|
|
var dHovered = destination + ' ul li.hovered:first';
|
|
|
|
// Send the message when enter pressend
|
|
$(inbox + 'inbox-new input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
if(exists(dHovered))
|
|
addBuddySearch(destination, $(dHovered).attr('data-xid'));
|
|
else
|
|
checkInboxMessage();
|
|
}
|
|
});
|
|
|
|
// Buddy search
|
|
$(inbox + 'inbox-new-to-input').keyup(function(e) {
|
|
if(e.keyCode != 13) {
|
|
// New buddy search
|
|
if((e.keyCode != 40) && (e.keyCode != 38))
|
|
createBuddySearch(destination);
|
|
|
|
// Navigating with keyboard in the results
|
|
arrowsBuddySearch(e, destination);
|
|
}
|
|
})
|
|
|
|
// Buddy search lost focus
|
|
.blur(function() {
|
|
if(!$(destination + ' ul').attr('mouse-hover'))
|
|
resetBuddySearch(destination);
|
|
})
|
|
|
|
// Buddy search got focus
|
|
.focus(function() {
|
|
var value = $(this).val();
|
|
|
|
// Add a comma at the end
|
|
if(value && !value.match(/^(.+)((,)(\s)?)$/))
|
|
$(this).val(value + ', ');
|
|
});
|
|
|
|
// Click events
|
|
$(inbox + 'a-delete-messages').click(purgeInbox);
|
|
$(inbox + 'a-new-message').click(newInboxMessage);
|
|
$(inbox + 'a-show-messages').click(showInboxMessages);
|
|
$(inbox + 'inbox-new-send a').click(checkInboxMessage);
|
|
|
|
$(inbox + 'bottom .finish').click(function() {
|
|
return closeInbox();
|
|
});
|
|
|
|
// File upload
|
|
var attach_options = {
|
|
dataType: 'xml',
|
|
beforeSubmit: waitInboxAttach,
|
|
success: handleInboxAttach
|
|
};
|
|
|
|
// Upload form submit event
|
|
$('#inbox .inbox-new-file').submit(function() {
|
|
if($('#inbox .wait').is(':hidden') && $('#inbox .inbox-new-file input[type="file"]').val())
|
|
$(this).ajaxSubmit(attach_options);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Upload input change event
|
|
$('#inbox .inbox-new-file input[type="file"]').change(function() {
|
|
if($('#inbox .wait').is(':hidden') && $(this).val())
|
|
$('#inbox .inbox-new-file').ajaxSubmit(attach_options);
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the microblog JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, Maranda
|
|
Last revision: 16/05/13
|
|
|
|
*/
|
|
|
|
// Completes arrays of an entry's attached files
|
|
function attachedMicroblog(selector, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments) {
|
|
if($(selector).attr('title'))
|
|
tFName.push($(selector).attr('title'));
|
|
else
|
|
tFName.push('');
|
|
|
|
if($(selector).attr('href'))
|
|
tFURL.push($(selector).attr('href'));
|
|
else
|
|
tFURL.push('');
|
|
|
|
if($(selector).find('link[rel="self"][title="thumb"]:first').attr('href'))
|
|
tFThumb.push($(selector).find('link[rel="self"][title="thumb"]:first').attr('href'));
|
|
else
|
|
tFThumb.push('');
|
|
|
|
if($(selector).attr('source'))
|
|
tFSource.push($(selector).attr('source'));
|
|
else
|
|
tFSource.push('');
|
|
|
|
if($(selector).attr('type'))
|
|
tFType.push($(selector).attr('type'));
|
|
else
|
|
tFType.push('');
|
|
|
|
if($(selector).attr('length'))
|
|
tFLength.push($(selector).attr('length'));
|
|
else
|
|
tFLength.push('');
|
|
|
|
// Comments?
|
|
var comments_href_c = $(selector).find('link[rel="replies"][title="comments_file"]:first').attr('href');
|
|
|
|
if(comments_href_c && comments_href_c.match(/^xmpp:(.+)\?;node=(.+)/)) {
|
|
tFEComments.push(RegExp.$1);
|
|
tFNComments.push(decodeURIComponent(RegExp.$2));
|
|
}
|
|
|
|
else {
|
|
tFEComments.push('');
|
|
tFNComments.push('');
|
|
}
|
|
}
|
|
|
|
// Displays a given microblog item
|
|
function displayMicroblog(packet, from, hash, mode, way) {
|
|
// Get some values
|
|
var iParse = $(packet.getNode()).find('items item');
|
|
|
|
iParse.each(function() {
|
|
// Initialize
|
|
var tContent, tFiltered, tTime, tDate, tStamp, tBody, tName, tID, tHash, tIndividual, tFEClick;
|
|
var tHTMLEscape = false;
|
|
|
|
// Arrays
|
|
var tFName = [];
|
|
var tFURL = [];
|
|
var tFThumb = [];
|
|
var tFSource = [];
|
|
var tFType = [];
|
|
var tFLength = [];
|
|
var tFEComments = [];
|
|
var tFNComments = [];
|
|
var aFURL = [];
|
|
var aFCat = [];
|
|
|
|
// Get the values
|
|
tDate = $(this).find('published').text();
|
|
tBody = $(this).find('body').text();
|
|
tID = $(this).attr('id');
|
|
tName = getBuddyName(from);
|
|
tHash = 'update-' + hex_md5(tName + tDate + tID);
|
|
|
|
// Read attached files with a thumb (place them at first)
|
|
$(this).find('link[rel="enclosure"]:has(link[rel="self"][title="thumb"])').each(function() {
|
|
attachedMicroblog(this, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments);
|
|
});
|
|
|
|
// Read attached files without any thumb
|
|
$(this).find('link[rel="enclosure"]:not(:has(link[rel="self"][title="thumb"]))').each(function() {
|
|
attachedMicroblog(this, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments);
|
|
});
|
|
|
|
// Get the repeat value
|
|
var uRepeat = [$(this).find('author name').text(), explodeThis(':', $(this).find('author uri').text(), 1)];
|
|
var uRepeated = false;
|
|
|
|
if(!uRepeat[0])
|
|
uRepeat = [getBuddyName(from), uRepeat[1]];
|
|
if(!uRepeat[1])
|
|
uRepeat = [uRepeat[0], from];
|
|
|
|
// Repeated?
|
|
if(uRepeat[1] != from)
|
|
uRepeated = true;
|
|
|
|
// Get the comments node
|
|
var entityComments, nodeComments;
|
|
|
|
// Get the comments
|
|
var comments_href = $(this).find('link[title="comments"]:first').attr('href');
|
|
|
|
if(comments_href && comments_href.match(/^xmpp:(.+)\?;node=(.+)/)) {
|
|
entityComments = RegExp.$1;
|
|
nodeComments = decodeURIComponent(RegExp.$2);
|
|
}
|
|
|
|
// No comments node?
|
|
if(!entityComments || !nodeComments) {
|
|
entityComments = '';
|
|
nodeComments = '';
|
|
}
|
|
|
|
// Get the stamp & time
|
|
if(tDate) {
|
|
tStamp = extractStamp(Date.jab2date(tDate));
|
|
tTime = relativeDate(tDate);
|
|
}
|
|
|
|
else {
|
|
tStamp = getTimeStamp();
|
|
tTime = '';
|
|
}
|
|
|
|
// Get the item geoloc
|
|
var tGeoloc = '';
|
|
var sGeoloc = $(this).find('geoloc:first');
|
|
var gLat = sGeoloc.find('lat').text();
|
|
var gLon = sGeoloc.find('lon').text();
|
|
|
|
if(gLat && gLon) {
|
|
tGeoloc += '<a class="geoloc talk-images" href="http://maps.google.com/?q=' + encodeQuotes(gLat) + ',' + encodeQuotes(gLon) + '" target="_blank">';
|
|
|
|
// Human-readable name?
|
|
var gHuman = humanPosition(
|
|
sGeoloc.find('locality').text(),
|
|
sGeoloc.find('region').text(),
|
|
sGeoloc.find('country').text()
|
|
);
|
|
|
|
if(gHuman)
|
|
tGeoloc += gHuman.htmlEnc();
|
|
else
|
|
tGeoloc += gLat.htmlEnc() + '; ' + gLon.htmlEnc();
|
|
|
|
tGeoloc += '</a>';
|
|
}
|
|
|
|
// Entry content: HTML, parse!
|
|
if($(this).find('content[type="html"]').size()) {
|
|
// Filter the xHTML message
|
|
tContent = filterThisXHTML(this);
|
|
tHTMLEscape = false;
|
|
}
|
|
|
|
// Entry content: Fallback on PLAIN?
|
|
if(!tContent) {
|
|
tContent = $(this).find('content[type="text"]').text();
|
|
|
|
if(!tContent) {
|
|
// Legacy?
|
|
tContent = $(this).find('title:not(source > title)').text();
|
|
|
|
// Last chance?
|
|
if(!tContent)
|
|
tContent = tBody;
|
|
}
|
|
|
|
// Trim the content
|
|
tContent = trim(tContent);
|
|
tHTMLEscape = true;
|
|
}
|
|
|
|
// Any content?
|
|
if(tContent) {
|
|
// Apply links to message body
|
|
tFiltered = filterThisMessage(tContent, tName.htmlEnc(), tHTMLEscape);
|
|
|
|
// Display the received message
|
|
var html = '<div class="one-update update_' + hash + ' ' + tHash + '" data-stamp="' + encodeQuotes(tStamp) + '" data-id="' + encodeQuotes(tID) + '" data-xid="' + encodeQuotes(from) + '">' +
|
|
'<div class="' + hash + '">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="body">' +
|
|
'<p>';
|
|
|
|
// Is it a repeat?
|
|
if(uRepeated)
|
|
html += '<a href="#" class="repeat talk-images" title="' + encodeQuotes(printf(_e("This is a repeat from %s"), uRepeat[0] + ' (' + uRepeat[1] + ')')) + '" onclick="return checkChatCreate(\'' + encodeOnclick(uRepeat[1]) + '\', \'chat\');" data-xid="' + encodeQuotes(uRepeat[1]) + '"></a>';
|
|
|
|
html += '<b title="' + from + '" class="name">' + tName.htmlEnc() + '</b> <span>' + tFiltered + '</span></p>' +
|
|
'<p class="infos">' + tTime + tGeoloc + '</p>';
|
|
|
|
// Any file to display?
|
|
if(tFURL.length)
|
|
html += '<p class="file">';
|
|
|
|
// Generate an array of the files URL
|
|
for(var a = 0; a < tFURL.length; a++) {
|
|
// Not enough data?
|
|
if(!tFURL[a])
|
|
continue;
|
|
|
|
// Push the current URL! (YouTube or file)
|
|
if(tFURL[a].match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) {
|
|
aFURL.push(trim(RegExp.$8));
|
|
aFCat.push('youtube');
|
|
}
|
|
|
|
else if(canIntegrateBox(strAfterLast('.', tFURL[a]))) {
|
|
aFURL.push(tFURL[a]);
|
|
aFCat.push(fileCategory(strAfterLast('.', tFURL[a])));
|
|
}
|
|
}
|
|
|
|
// Add each file code
|
|
for(var f = 0; f < tFURL.length; f++) {
|
|
// Not enough data?
|
|
if(!tFURL[f])
|
|
continue;
|
|
|
|
// Get the file type
|
|
var tFLink = tFURL[f];
|
|
var tFExt = strAfterLast('.', tFLink);
|
|
var tFCat = fileCategory(tFExt);
|
|
|
|
// Youtube video?
|
|
if(tFLink.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) {
|
|
tFLink = trim(RegExp.$8);
|
|
tFCat = 'youtube';
|
|
}
|
|
|
|
// Supported image/video/sound
|
|
if(canIntegrateBox(tFExt) || (tFCat == 'youtube'))
|
|
tFEClick = 'onclick="return applyIntegrateBox(\'' + encodeOnclick(tFLink) + '\', \'' + encodeOnclick(tFCat) + '\', \'' + encodeOnclick(aFURL) + '\', \'' + encodeOnclick(aFCat) + '\', \'' + encodeOnclick(tFEComments) + '\', \'' + encodeOnclick(tFNComments) + '\', \'large\');" ';
|
|
else
|
|
tFEClick = '';
|
|
|
|
// Any thumbnail?
|
|
if(tFThumb[f])
|
|
html += '<a class="thumb" ' + tFEClick + 'href="' + encodeQuotes(tFURL[f]) + '" target="_blank" title="' + encodeQuotes(tFName[f]) + '" data-node="' + encodeQuotes(tFNComments[f]) + '"><img src="' + encodeQuotes(tFThumb[f]) + '" alt="" /></a>';
|
|
else
|
|
html += '<a class="' + encodeQuotes(tFCat) + ' link talk-images" ' + tFEClick + 'href="' + encodeQuotes(tFURL[f]) + '" target="_blank" data-node="' + encodeQuotes(tFNComments[f]) + '">' + tFName[f].htmlEnc() + '</a>';
|
|
}
|
|
|
|
if(tFURL.length)
|
|
html += '</p>';
|
|
|
|
// It's my own notice, we can remove it!
|
|
if(from == getXID())
|
|
html += '<a href="#" onclick="return removeMicroblog(\'' + encodeOnclick(tID) + '\', \'' + encodeOnclick(tHash) + '\', \'' + encodeOnclick(entityComments) + '\', \'' + encodeOnclick(nodeComments) + '\');" title="' + _e("Remove this notice") + '" class="mbtool remove talk-images"></a>';
|
|
|
|
// Notice from another user
|
|
else {
|
|
// User profile
|
|
html += '<a href="#" title="' + _e("View profile") + '" class="mbtool profile talk-images" onclick="return openUserInfos(\'' + encodeOnclick(from) + '\');"></a>';
|
|
|
|
// If PEP is enabled
|
|
if(enabledPEP() && tHTMLEscape)
|
|
html += '<a href="#" title="' + _e("Repeat this notice") + '" class="mbtool repost talk-images"></a>';
|
|
}
|
|
|
|
html += '</div><div class="comments-container" data-node="' + encodeQuotes(nodeComments) + '"></div></div>';
|
|
|
|
// Mixed mode
|
|
if((mode == 'mixed') && !exists('.mixed .' + tHash)) {
|
|
// Remove the old element
|
|
if(way == 'push')
|
|
$('#channel .content.mixed .one-update.update_' + hash).remove();
|
|
|
|
// Get the nearest element
|
|
var nearest = sortElementByStamp(tStamp, '#channel .mixed .one-update');
|
|
|
|
// Append the content at the right position (date relative)
|
|
if(nearest == 0)
|
|
$('#channel .content.mixed').append(html);
|
|
else
|
|
$('#channel .one-update[data-stamp="' + nearest + '"]:first').before(html);
|
|
|
|
// Show the new item
|
|
if(way == 'push')
|
|
$('#channel .content.mixed .one-update.' + tHash).fadeIn('fast');
|
|
else
|
|
$('#channel .content.mixed .one-update.' + tHash).show();
|
|
|
|
// Remove the old notices to make the DOM lighter
|
|
var oneUpdate = '#channel .content.mixed .one-update';
|
|
|
|
if($(oneUpdate).size() > 80)
|
|
$(oneUpdate + ':last').remove();
|
|
|
|
// Click event on avatar/name
|
|
$('.mixed .' + tHash + ' .avatar-container, .mixed .' + tHash + ' .body b').click(function() {
|
|
getMicroblog(from, hash);
|
|
});
|
|
}
|
|
|
|
// Individual mode
|
|
tIndividual = '#channel .content.individual.microblog-' + hash;
|
|
|
|
// Can append individual content?
|
|
var can_individual = true;
|
|
|
|
if($('#channel .top.individual input[name="comments"]').val() && exists(tIndividual + ' .one-update'))
|
|
can_individual = false;
|
|
|
|
if(can_individual && exists(tIndividual) && !exists('.individual .' + tHash)) {
|
|
if(mode == 'mixed')
|
|
$(tIndividual).prepend(html);
|
|
else
|
|
$(tIndividual + ' a.more').before(html);
|
|
|
|
// Show the new item
|
|
if(way == 'push')
|
|
$('#channel .content.individual .one-update.' + tHash).fadeIn('fast');
|
|
else
|
|
$('#channel .content.individual .one-update.' + tHash).show();
|
|
|
|
// Make 'more' link visible
|
|
$(tIndividual + ' a.more').css('visibility', 'visible');
|
|
|
|
// Click event on name (if not me!)
|
|
if(from != getXID())
|
|
$('.individual .' + tHash + ' .avatar-container, .individual .' + tHash + ' .body b').click(function() {
|
|
checkChatCreate(from, 'chat');
|
|
});
|
|
}
|
|
|
|
// Apply the click event
|
|
$('.' + tHash + ' a.repost:not([data-event="true"])').click(function() {
|
|
return publishMicroblog(tContent, tFName, tFURL, tFType, tFLength, tFThumb, uRepeat, entityComments, nodeComments, tFEComments, tFNComments);
|
|
})
|
|
|
|
.attr('data-event', 'true');
|
|
|
|
// Apply the hover event
|
|
if(nodeComments)
|
|
$('.' + mode + ' .' + tHash).hover(function() {
|
|
showCommentsMicroblog($(this), entityComments, nodeComments, tHash);
|
|
}, function() {
|
|
if($(this).find('div.comments a.one-comment.loading').size())
|
|
$(this).find('div.comments').remove();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Display the avatar of this buddy
|
|
getAvatar(from, 'cache', 'true', 'forget');
|
|
}
|
|
|
|
// Removes a given microblog item
|
|
function removeMicroblog(id, hash, pserver, cnode) {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#publisher-delete */
|
|
|
|
// Initialize
|
|
var selector = $('.' + hash);
|
|
var get_last = false;
|
|
|
|
// Get the latest item for the mixed mode
|
|
if(exists('#channel .content.mixed .' + hash))
|
|
get_last = true;
|
|
|
|
// Remove the item from our DOM
|
|
selector.fadeOut('fast', function() {
|
|
$(this).remove();
|
|
});
|
|
|
|
// Send the IQ to remove the item (and get eventual error callback)
|
|
// Also attempt to remove the comments node.
|
|
var retract_iq = new JSJaCIQ();
|
|
retract_iq.setType('set');
|
|
retract_iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}).appendChild(retract_iq.buildNode('retract', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB})).appendChild(retract_iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB}));
|
|
|
|
var comm_delete_iq;
|
|
if (pserver != '' && cnode != '') {
|
|
comm_delete_iq = new JSJaCIQ();
|
|
comm_delete_iq.setType('set');
|
|
comm_delete_iq.setTo(pserver);
|
|
comm_delete_iq.appendNode('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub#owner'}).appendChild(comm_delete_iq.buildNode('delete', {'node': cnode, 'xmlns': 'http://jabber.org/protocol/pubsub#owner'}));
|
|
}
|
|
|
|
if(get_last) {
|
|
if (comm_delete_iq) { con.send(comm_delete_iq); }
|
|
con.send(retract_iq, handleRemoveMicroblog);
|
|
} else {
|
|
if (comm_delete_iq) { con.send(comm_delete_iq); }
|
|
con.send(retract_iq, handleErrorReply);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the microblog item removal
|
|
function handleRemoveMicroblog(iq) {
|
|
// Handle the error reply
|
|
handleErrorReply(iq);
|
|
|
|
// Get the latest item
|
|
requestMicroblog(getXID(), '1', false, handleUpdateRemoveMicroblog);
|
|
}
|
|
|
|
// Handles the microblog update
|
|
function handleUpdateRemoveMicroblog(iq) {
|
|
// Error?
|
|
if(iq.getType() == 'error')
|
|
return;
|
|
|
|
// Initialize
|
|
var xid = bareXID(getStanzaFrom(iq));
|
|
var hash = hex_md5(xid);
|
|
|
|
// Display the item!
|
|
displayMicroblog(iq, xid, hash, 'mixed', 'push');
|
|
}
|
|
|
|
// Gets a given microblog comments node
|
|
function getCommentsMicroblog(server, node, id) {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve-requestall */
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
iq.setID('get_' + genID() + '-' + id);
|
|
iq.setTo(server);
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
pubsub.appendChild(iq.buildNode('items', {'node': node, 'xmlns': NS_PUBSUB}));
|
|
|
|
con.send(iq, handleCommentsMicroblog);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles a microblog comments node items
|
|
function handleCommentsMicroblog(iq) {
|
|
// Path
|
|
var id = explodeThis('-', iq.getID(), 1);
|
|
var path = 'div.comments[data-id="' + id + '"] div.comments-content';
|
|
|
|
// Does not exist?
|
|
if(!exists(path))
|
|
return false;
|
|
|
|
// Any error?
|
|
if(handleErrorReply(iq)) {
|
|
$(path).html('<div class="one-comment loading">' + _e("Could not get the comments!") + '</div>');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Initialize
|
|
var data = iq.getNode();
|
|
var server = bareXID(getStanzaFrom(iq));
|
|
var node = $(data).find('items:first').attr('node');
|
|
var users_xid = [];
|
|
var code = '';
|
|
|
|
// No node?
|
|
if(!node)
|
|
node = $(data).find('publish:first').attr('node');
|
|
|
|
// Get the parent microblog item
|
|
var parent_select = $('#channel .one-update:has(*[data-node="' + node + '"])');
|
|
var parent_data = [parent_select.attr('data-xid'), NS_URN_MBLOG, parent_select.attr('data-id')];
|
|
|
|
// Get the owner XID
|
|
var owner_xid = parent_select.attr('data-xid');
|
|
var repeat_xid = parent_select.find('a.repeat').attr('data-xid');
|
|
|
|
// Must we create the complete DOM?
|
|
var complete = true;
|
|
|
|
if($(path).find('.one-comment.compose').size())
|
|
complete = false;
|
|
|
|
// Add the comment tool
|
|
if(complete)
|
|
code += '<div class="one-comment compose">' +
|
|
'<span class="icon talk-images"></span><input type="text" placeholder="' + _e("Type your comment here...") + '" />' +
|
|
'</div>';
|
|
|
|
// Append the comments
|
|
$(data).find('item').each(function() {
|
|
// Get comment
|
|
var current_id = $(this).attr('id');
|
|
var current_xid = explodeThis(':', $(this).find('author uri').text(), 1);
|
|
var current_name = $(this).find('author name').text();
|
|
var current_date = $(this).find('published').text();
|
|
var current_body = $(this).find('content[type="text"]').text();
|
|
var current_bname = getBuddyName(current_xid);
|
|
|
|
// Legacy?
|
|
if(!current_body)
|
|
current_body = $(this).find('title:not(source > title)').text();
|
|
|
|
// Yet displayed? (continue the loop)
|
|
if($(path).find('.one-comment[data-id="' + current_id + '"]').size())
|
|
return;
|
|
|
|
// No XID?
|
|
if(!current_xid) {
|
|
current_xid = '';
|
|
|
|
if(!current_name)
|
|
current_name = _e("unknown");
|
|
}
|
|
|
|
else if(!current_name || (current_bname != getXIDNick(current_xid)))
|
|
current_name = current_bname;
|
|
|
|
// Any date?
|
|
if(current_date)
|
|
current_date = relativeDate(current_date);
|
|
else
|
|
current_date = getCompleteTime();
|
|
|
|
// Click event
|
|
var onclick = 'false';
|
|
|
|
if(current_xid != getXID())
|
|
onclick = 'checkChatCreate(\'' + encodeOnclick(current_xid) + '\', \'chat\')';
|
|
|
|
// If this is my comment, add a marker
|
|
var type = 'him';
|
|
var marker = '';
|
|
var remove = '';
|
|
|
|
if(current_xid == getXID()) {
|
|
type = 'me';
|
|
marker = '<div class="marker"></div>';
|
|
remove = '<a href="#" class="remove" onclick="return removeCommentMicroblog(\'' + encodeOnclick(server) + '\', \'' + encodeOnclick(node) + '\', \'' + encodeOnclick(current_id) + '\');">' + _e("Remove") + '</a>';
|
|
}
|
|
|
|
// New comment?
|
|
var new_class = '';
|
|
|
|
if(!complete)
|
|
new_class = ' new';
|
|
|
|
// Add the comment
|
|
if(current_body) {
|
|
// Add the XID
|
|
if(!existArrayValue(users_xid, current_xid))
|
|
users_xid.push(current_xid);
|
|
|
|
// Add the HTML code
|
|
code += '<div class="one-comment ' + hex_md5(current_xid) + ' ' + type + new_class + '" data-id="' + encodeQuotes(current_id) + '">' +
|
|
marker +
|
|
|
|
'<div class="avatar-container" onclick="return ' + onclick + ';">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="comment-container">' +
|
|
'<a href="#" onclick="return ' + onclick + ';" title="' + encodeQuotes(current_xid) + '" class="name">' + current_name.htmlEnc() + '</a>' +
|
|
'<span class="date">' + current_date.htmlEnc() + '</span>' +
|
|
remove +
|
|
|
|
'<p class="body">' + filterThisMessage(current_body, current_name, true) + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="clear"></div>' +
|
|
'</div>';
|
|
}
|
|
});
|
|
|
|
// Add the HTML
|
|
if(complete) {
|
|
$(path).html(code);
|
|
|
|
// Focus on the compose input
|
|
$(document).oneTime(10, function() {
|
|
$(path).find('.one-comment.compose input').focus();
|
|
});
|
|
}
|
|
|
|
else {
|
|
$(path).find('.one-comment.compose').after(code);
|
|
|
|
// Beautiful effect
|
|
$(path).find('.one-comment.new').slideDown('fast', function() {
|
|
adaptCommentMicroblog(id);
|
|
}).removeClass('new');
|
|
}
|
|
|
|
// Set the good widths
|
|
adaptCommentMicroblog(id);
|
|
|
|
// Get the avatars
|
|
for(a in users_xid)
|
|
getAvatar(users_xid[a], 'cache', 'true', 'forget');
|
|
|
|
// Add the owner XID
|
|
if(owner_xid && owner_xid.match('@') && !existArrayValue(users_xid, owner_xid))
|
|
users_xid.push(owner_xid);
|
|
|
|
// Add the repeated from XID
|
|
if(repeat_xid && repeat_xid.match('@') && !existArrayValue(users_xid, repeat_xid))
|
|
users_xid.push(repeat_xid);
|
|
|
|
// Remove my own XID
|
|
removeArrayValue(users_xid, getXID());
|
|
|
|
// DOM events
|
|
if(complete) {
|
|
// Update timer
|
|
$(path).everyTime('60s', function() {
|
|
getCommentsMicroblog(server, node, id);
|
|
|
|
logThis('Updating comments node: ' + node + ' on ' + server + '...');
|
|
});
|
|
|
|
// Input key event
|
|
$(path).find('.one-comment.compose input').placeholder()
|
|
.keyup(function(e) {
|
|
if((e.keyCode == 13) && $(this).val()) {
|
|
// Send the comment!
|
|
sendCommentMicroblog($(this).val(), server, node, id, users_xid, parent_data);
|
|
|
|
// Reset the input value
|
|
$(this).val('');
|
|
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Shows the microblog comments box
|
|
function showCommentsMicroblog(path, entityComments, nodeComments, tHash) {
|
|
// Do not display it twice!
|
|
if(path.find('div.comments').size())
|
|
return;
|
|
|
|
// Generate an unique ID
|
|
var idComments = genID();
|
|
|
|
// Create comments container
|
|
path.find('div.comments-container').append(
|
|
'<div class="comments" data-id="' + encodeQuotes(idComments) + '">' +
|
|
'<div class="arrow talk-images"></div>' +
|
|
'<div class="comments-content">' +
|
|
'<a href="#" class="one-comment loading"><span class="icon talk-images"></span>' + _e("Show comments") + '</a>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Click event
|
|
path.find('div.comments a.one-comment').click(function() {
|
|
// Set loading info
|
|
$(this).parent().html('<div class="one-comment loading"><span class="icon talk-images"></span>' + _e("Loading comments...") + '</div>');
|
|
|
|
// Request comments
|
|
getCommentsMicroblog(entityComments, nodeComments, idComments);
|
|
|
|
// Remove the comments from the DOM if click away
|
|
if(tHash) {
|
|
$('#channel').off('click');
|
|
|
|
$('#channel').on('click', function(evt) {
|
|
if(!$(evt.target).parents('.' + tHash).size()) {
|
|
$('#channel').off('click');
|
|
$('#channel .one-update div.comments-content').stopTime();
|
|
$('#channel .one-update div.comments').remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Sends a comment on a given microblog comments node
|
|
function sendCommentMicroblog(value, server, node, id, notifiy_arr, parent_data) {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#publisher-publish */
|
|
|
|
// Not enough data?
|
|
if(!value || !server || !node)
|
|
return false;
|
|
|
|
// Get some values
|
|
var date = getXMPPTime('utc');
|
|
var hash = hex_md5(value + date);
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
iq.setTo(server);
|
|
iq.setID('set_' + genID() + '-' + id);
|
|
|
|
// PubSub main elements
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': node, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'id': hash, 'xmlns': NS_PUBSUB}));
|
|
var entry = item.appendChild(iq.buildNode('entry', {'xmlns': NS_ATOM}));
|
|
|
|
// Author infos
|
|
var author = entry.appendChild(iq.buildNode('author', {'xmlns': NS_ATOM}));
|
|
author.appendChild(iq.buildNode('name', {'xmlns': NS_ATOM}, getName()));
|
|
author.appendChild(iq.buildNode('uri', {'xmlns': NS_ATOM}, 'xmpp:' + getXID()));
|
|
|
|
// Create the comment
|
|
entry.appendChild(iq.buildNode('content', {'type': 'text', 'xmlns': NS_ATOM}, value));
|
|
entry.appendChild(iq.buildNode('published', {'xmlns': NS_ATOM}, date));
|
|
|
|
con.send(iq);
|
|
|
|
// Handle this comment!
|
|
iq.setFrom(server);
|
|
handleCommentsMicroblog(iq);
|
|
|
|
// Notify users
|
|
if(notifiy_arr && notifiy_arr.length) {
|
|
// XMPP link to the item
|
|
var href = 'xmpp:' + server + '?;node=' + encodeURIComponent(node) + ';item=' + encodeURIComponent(hash);
|
|
|
|
// Loop!
|
|
for(n in notifiy_arr)
|
|
sendNotification(notifiy_arr[n], 'comment', href, value, parent_data);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Removes a given microblog comment item
|
|
function removeCommentMicroblog(server, node, id) {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#publisher-delete */
|
|
|
|
// Remove the item from our DOM
|
|
$('.one-comment[data-id="' + id + '"]').slideUp('fast', function() {
|
|
// Get the parent ID
|
|
var parent_id = $(this).parents('div.comments').attr('data-id');
|
|
|
|
// Remove it!
|
|
$(this).remove();
|
|
|
|
// Adapt the width
|
|
adaptCommentMicroblog(parent_id);
|
|
});
|
|
|
|
// Send the IQ to remove the item (and get eventual error callback)
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
iq.setTo(server);
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var retract = pubsub.appendChild(iq.buildNode('retract', {'node': node, 'xmlns': NS_PUBSUB}));
|
|
retract.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB}));
|
|
|
|
con.send(iq);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Adapts the comment elements width
|
|
function adaptCommentMicroblog(id) {
|
|
var selector = $('div.comments[data-id="' + id + '"] div.comments-content');
|
|
var selector_width = selector.width();
|
|
|
|
// Change widths
|
|
selector.find('.one-comment.compose input').css('width', selector_width - 60);
|
|
selector.find('.one-comment .comment-container').css('width', selector_width - 55);
|
|
}
|
|
|
|
// Handles the microblog of an user
|
|
function handleMicroblog(iq) {
|
|
// Get the from attribute of this IQ
|
|
var from = bareXID(getStanzaFrom(iq));
|
|
|
|
// Define the selector path
|
|
var selector = '#channel .top.individual input[name=';
|
|
|
|
// Is this request still alive?
|
|
if(from == $(selector + 'jid]').val()) {
|
|
var hash = hex_md5(from);
|
|
|
|
// Update the items counter
|
|
var old_count = parseInt($(selector + 'counter]').val());
|
|
$(selector + 'counter]').val(old_count + 20);
|
|
|
|
// Display the microblog
|
|
displayMicroblog(iq, from, hash, 'individual', 'request');
|
|
|
|
// Hide the waiting icon
|
|
if(enabledPEP())
|
|
waitMicroblog('sync');
|
|
else
|
|
waitMicroblog('unsync');
|
|
|
|
// Hide the 'more items' link?
|
|
if($(iq.getNode()).find('item').size() < old_count)
|
|
$('#channel .individual a.more').remove();
|
|
|
|
// Get the comments?
|
|
var comments_node = $('#channel .top.individual input[name="comments"]').val();
|
|
|
|
if(comments_node && comments_node.match(/^xmpp:(.+)\?;node=(.+);item=(.+)/)) {
|
|
// Get the values
|
|
var comments_entity = RegExp.$1;
|
|
comments_node = decodeURIComponent(RegExp.$2);
|
|
|
|
// Selectors
|
|
var file_link = $('#channel .individual .one-update p.file a[data-node="' + comments_node + '"]');
|
|
var entry_link = $('#channel .individual .one-update:has(.comments-container[data-node="' + comments_node + '"])');
|
|
|
|
// Is it a microblog entry (or a lonely entry file)?
|
|
if(entry_link.size()) {
|
|
showCommentsMicroblog(entry_link, comments_entity, comments_node);
|
|
entry_link.find('a.one-comment').click();
|
|
}
|
|
|
|
// Is it a file?
|
|
else if(file_link.size())
|
|
file_link.click();
|
|
}
|
|
}
|
|
|
|
logThis('Microblog got: ' + from, 3);
|
|
}
|
|
|
|
// Handles the microblog of an user (from roster)
|
|
function handleRosterMicroblog(iq) {
|
|
// Get the from attribute of this IQ
|
|
var from = bareXID(getStanzaFrom(iq));
|
|
|
|
// Display the microblog
|
|
displayMicroblog(iq, from, hex_md5(from), 'mixed', 'push');
|
|
}
|
|
|
|
// Resets the microblog elements
|
|
function resetMicroblog() {
|
|
// Reset everything
|
|
$('#channel .individual .one-update div.comments-content').stopTime();
|
|
$('#channel .individual').remove();
|
|
$('#channel .mixed').show();
|
|
|
|
// Hide the waiting icon
|
|
if(enabledPEP())
|
|
waitMicroblog('sync');
|
|
else
|
|
waitMicroblog('unsync');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the user's microblog to check it exists
|
|
function getInitMicroblog() {
|
|
getMicroblog(getXID(), hex_md5(getXID()), true);
|
|
}
|
|
|
|
// Handles the user's microblog to create it in case of error
|
|
function handleInitMicroblog(iq) {
|
|
// Any error?
|
|
if((iq.getType() == 'error') && $(iq.getNode()).find('item-not-found').size()) {
|
|
// The node may not exist, create it!
|
|
setupMicroblog('', NS_URN_MBLOG, '1', '1000000', '', '', true);
|
|
|
|
logThis('Error while getting microblog, trying to reconfigure the PubSub node!', 2);
|
|
}
|
|
}
|
|
|
|
// Requests an user's microblog
|
|
function requestMicroblog(xid, items, get_item, handler) {
|
|
// Ask the server the user's microblog
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
iq.setTo(xid);
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var ps_items = pubsub.appendChild(iq.buildNode('items', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB}));
|
|
|
|
// Request a particular item?
|
|
if(get_item)
|
|
ps_items.appendChild(iq.buildNode('item', {'id': get_item, 'xmlns': NS_PUBSUB}));
|
|
else
|
|
ps_items.setAttribute('max_items', items);
|
|
|
|
if(handler)
|
|
con.send(iq, handler);
|
|
else
|
|
con.send(iq, handleMicroblog);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the microblog of an user
|
|
function getMicroblog(xid, hash, check) {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve */
|
|
|
|
logThis('Get the microblog: ' + xid, 3);
|
|
|
|
// Fire the wait event
|
|
waitMicroblog('fetch');
|
|
|
|
// XMPP URI?
|
|
var get_item = '';
|
|
|
|
if(xid.match(/^xmpp:(.+)\?;node=(.+);item=(.+)/)) {
|
|
xid = RegExp.$1;
|
|
get_item = decodeURIComponent(RegExp.$3);
|
|
}
|
|
|
|
// No hash?
|
|
if(!hash)
|
|
hash = hex_md5(xid);
|
|
|
|
// Can display the individual channel?
|
|
if(!check && !exists('#channel .individual')) {
|
|
// Hide the mixed channel
|
|
$('#channel .mixed').hide();
|
|
|
|
// Get the channel title depending on the XID
|
|
var cTitle;
|
|
var cShortcuts = '';
|
|
|
|
if(xid == getXID())
|
|
cTitle = _e("Your channel");
|
|
else {
|
|
cTitle = _e("Channel of") + ' ' + getBuddyName(xid).htmlEnc();
|
|
cShortcuts = '<div class="shortcuts">' +
|
|
'<a href="#" class="message talk-images" title="' + _e("Send him/her a message") + '" onclick="return composeInboxMessage(\'' + encodeOnclick(xid) + '\');"></a>' +
|
|
'<a href="#" class="chat talk-images" title="' + _e("Start a chat with him/her") + '" onclick="return checkChatCreate(\'' + encodeOnclick(xid) + '\', \'chat\');"></a>' +
|
|
'<a href="#" class="command talk-images" title="' + _e("Command") + '" onclick="return retrieveAdHoc(\'' + encodeOnclick(xid) + '\');"></a>' +
|
|
'<a href="#" class="profile talk-images" title="' + _e("Show user profile") + '" onclick="return openUserInfos(\'' + encodeOnclick(xid) + '\');"></a>' +
|
|
'</div>';
|
|
}
|
|
|
|
// Create a new individual channel
|
|
$('#channel .content.mixed').after(
|
|
'<div class="content individual microblog-' + hash + '">' +
|
|
'<a href="#" class="more home-images" onclick="if($(\'#channel .footer div.fetch\').is(\':hidden\')) { return getMicroblog(\'' + encodeOnclick(xid) + '\', \'' + encodeOnclick(hash) + '\'); } return false;">' + _e("More notices...") + '</a>' +
|
|
'</div>'
|
|
)
|
|
|
|
.before(
|
|
'<div class="top individual ' + hash + '">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
|
|
'<div class="update">' +
|
|
'<h2>' + cTitle + '</h2>' +
|
|
'<a href="#" onclick="return resetMicroblog();">« ' + _e("Previous") + '</a>' +
|
|
'</div>' +
|
|
|
|
cShortcuts +
|
|
|
|
'<input type="hidden" name="jid" value="' + encodeQuotes(xid) + '" />' +
|
|
'<input type="hidden" name="counter" value="20" />' +
|
|
'</div>'
|
|
);
|
|
|
|
// Microblog navigation
|
|
$('#channel .content.individual').scroll(function() {
|
|
if($('#channel .footer div.fetch').is(':hidden') && $('#channel .individual a.more:visible').size() && $('#channel .content.individual').scrollTop() >= ($('#channel .content.individual')[0].scrollHeight - $('#channel .content.individual').height() - 200))
|
|
$('#channel .individual a.more').click();
|
|
});
|
|
|
|
// Display the user avatar
|
|
getAvatar(xid, 'cache', 'true', 'forget');
|
|
}
|
|
|
|
// Get the number of items to retrieve
|
|
var items = '0';
|
|
|
|
if(!check)
|
|
items = $('#channel .top.individual input[name="counter"]').val();
|
|
|
|
// Request
|
|
if(check)
|
|
requestMicroblog(xid, items, get_item, handleInitMicroblog);
|
|
else
|
|
requestMicroblog(xid, items, get_item, handleMicroblog);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Show a given microblog waiting status
|
|
function waitMicroblog(type) {
|
|
// First hide all the infos elements
|
|
$('#channel .footer div').hide();
|
|
|
|
// Display the good one
|
|
$('#channel .footer div.' + type).show();
|
|
|
|
// Depending on the type, disable/enable certain tools
|
|
var selector = $('#channel .top input[name="microblog_body"]');
|
|
|
|
if(type == 'unsync')
|
|
selector.attr('disabled', true);
|
|
else if(type == 'sync')
|
|
$(document).oneTime(10, function() {
|
|
selector.removeAttr('disabled').focus();
|
|
});
|
|
}
|
|
|
|
// Setups a new microblog
|
|
function setupMicroblog(entity, node, persist, maximum, access, publish, create) {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#owner-create-and-configure */
|
|
|
|
// Create the PubSub node
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// Any external entity?
|
|
if(entity)
|
|
iq.setTo(entity);
|
|
|
|
// Create it?
|
|
if(create) {
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
pubsub.appendChild(iq.buildNode('create', {'xmlns': NS_PUBSUB, 'node': node}));
|
|
}
|
|
|
|
else
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB_OWNER});
|
|
|
|
// Configure it!
|
|
var configure = pubsub.appendChild(iq.buildNode('configure', {'node': node, 'xmlns': NS_PUBSUB}));
|
|
var x = configure.appendChild(iq.buildNode('x', {'xmlns': NS_XDATA, 'type': 'submit'}));
|
|
|
|
var field1 = x.appendChild(iq.buildNode('field', {'var': 'FORM_TYPE', 'type': 'hidden', 'xmlns': NS_XDATA}));
|
|
field1.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, NS_PUBSUB_NC));
|
|
|
|
// Persist items?
|
|
if(persist) {
|
|
var field2 = x.appendChild(iq.buildNode('field', {'var': 'pubsub#persist_items', 'xmlns': NS_XDATA}));
|
|
field2.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, persist));
|
|
}
|
|
|
|
// Maximum items?
|
|
if(maximum) {
|
|
var field3 = x.appendChild(iq.buildNode('field', {'var': 'pubsub#max_items', 'xmlns': NS_XDATA}));
|
|
field3.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, maximum));
|
|
}
|
|
|
|
// Access rights?
|
|
if(access) {
|
|
var field4 = x.appendChild(iq.buildNode('field', {'var': 'pubsub#access_model', 'xmlns': NS_XDATA}));
|
|
field4.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, access));
|
|
}
|
|
|
|
// Publish rights?
|
|
if(publish) {
|
|
var field5 = x.appendChild(iq.buildNode('field', {'var': 'pubsub#publish_model', 'xmlns': NS_XDATA}));
|
|
field5.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, publish));
|
|
}
|
|
|
|
con.send(iq);
|
|
}
|
|
|
|
// Gets the microblog configuration
|
|
function getConfigMicroblog() {
|
|
// Lock the microblog options
|
|
$('#persistent, #maxnotices').attr('disabled', true);
|
|
|
|
// Get the microblog configuration
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB_OWNER});
|
|
pubsub.appendChild(iq.buildNode('configure', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB_OWNER}));
|
|
|
|
con.send(iq, handleGetConfigMicroblog);
|
|
}
|
|
|
|
// Handles the microblog configuration
|
|
function handleGetConfigMicroblog(iq) {
|
|
// Reset the options stuffs
|
|
waitOptions('microblog');
|
|
|
|
// Unlock the microblog options
|
|
$('#persistent, #maxnotices').removeAttr('disabled');
|
|
|
|
// End if not a result
|
|
if(!iq || (iq.getType() != 'result'))
|
|
return;
|
|
|
|
// Initialize the values
|
|
var selector = $(iq.getNode());
|
|
var persistent = '0';
|
|
var maxnotices = '1000000';
|
|
|
|
// Get the values
|
|
var xPersistent = selector.find('field[var="pubsub#persist_items"] value:first').text();
|
|
var xMaxnotices = selector.find('field[var="pubsub#max_items"] value:first').text();
|
|
|
|
// Any value?
|
|
if(xPersistent)
|
|
persistent = xPersistent;
|
|
|
|
if(xMaxnotices)
|
|
maxnotices = xMaxnotices;
|
|
|
|
// Change the maxnotices value
|
|
switch(maxnotices) {
|
|
case '1':
|
|
case '100':
|
|
case '1000':
|
|
case '10000':
|
|
case '100000':
|
|
case '1000000':
|
|
break;
|
|
|
|
default:
|
|
maxnotices = '1000000';
|
|
break;
|
|
}
|
|
|
|
// Apply persistent value
|
|
if(persistent == '0')
|
|
$('#persistent').attr('checked', false);
|
|
else
|
|
$('#persistent').attr('checked', true);
|
|
|
|
// Apply maxnotices value
|
|
$('#maxnotices').val(maxnotices);
|
|
}
|
|
|
|
// Handles the user's microblog
|
|
function handleMyMicroblog(packet) {
|
|
// Reset the entire form
|
|
$('#channel .top input[name="microblog_body"]').removeAttr('disabled').val('');
|
|
$('#channel .top input[name="microblog_body"]').placeholder();
|
|
unattachMicroblog();
|
|
|
|
// Check for errors
|
|
handleErrorReply(packet);
|
|
}
|
|
|
|
// Performs the microblog sender checks
|
|
function sendMicroblog() {
|
|
logThis('Send a new microblog item', 3);
|
|
|
|
// Avoid nasty errors
|
|
try {
|
|
// Get the values
|
|
var selector = $('#channel .top input[name="microblog_body"]');
|
|
var body = trim(selector.val());
|
|
|
|
// Sufficient parameters
|
|
if(body) {
|
|
// Disable & blur our input
|
|
selector.attr('disabled', true).blur();
|
|
|
|
// Files array
|
|
var fName = [];
|
|
var fType = [];
|
|
var fLength = [];
|
|
var fURL = [];
|
|
var fThumb = [];
|
|
|
|
// Read the files
|
|
$('#attach .one-file').each(function() {
|
|
// Push the values!
|
|
fName.push($(this).find('a.link').text());
|
|
fType.push($(this).attr('data-type'));
|
|
fLength.push($(this).attr('data-length'));
|
|
fURL.push($(this).find('a.link').attr('href'));
|
|
fThumb.push($(this).attr('data-thumb'));
|
|
});
|
|
|
|
// Containing YouTube videos?
|
|
var yt_matches = body.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim);
|
|
|
|
for(y in yt_matches) {
|
|
fName.push('');
|
|
fType.push('text/html');
|
|
fLength.push('');
|
|
fURL.push(trim(yt_matches[y]));
|
|
fThumb.push('https://img.youtube.com/vi/' + trim(yt_matches[y].replace(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim, '$8')) + '/0.jpg');
|
|
}
|
|
|
|
// Send the message on the XMPP network
|
|
publishMicroblog(body, fName, fURL, fType, fLength, fThumb);
|
|
}
|
|
}
|
|
|
|
// Return false (security)
|
|
finally {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Publishes a given microblog item
|
|
function publishMicroblog(body, attachedname, attachedurl, attachedtype, attachedlength, attachedthumb, repeat, comments_entity, comments_node, comments_entity_file, comments_node_file) {
|
|
/* REF: http://xmpp.org/extensions/xep-0277.html */
|
|
|
|
// Generate some values
|
|
var time = getXMPPTime('utc');
|
|
var id = hex_md5(body + time);
|
|
var nick = getName();
|
|
var xid = getXID();
|
|
|
|
// Define repeat options
|
|
var author_nick = nick;
|
|
var author_xid = xid;
|
|
|
|
if(repeat && repeat.length) {
|
|
author_nick = repeat[0];
|
|
author_xid = repeat[1];
|
|
}
|
|
|
|
// Define comments options
|
|
var node_create = false;
|
|
|
|
if(!comments_entity || !comments_node) {
|
|
node_create = true;
|
|
comments_entity = HOST_PUBSUB;
|
|
comments_node = NS_URN_MBLOG + ':comments/' + id;
|
|
}
|
|
|
|
if(!comments_entity_file)
|
|
comments_entity_file = [];
|
|
if(!comments_node_file)
|
|
comments_node_file = [];
|
|
|
|
// Don't create another comments node if only 1 file is attached
|
|
if(attachedurl && (attachedurl.length == 1) && (!comments_entity_file[0] || !comments_node_file[0])) {
|
|
comments_entity_file = [comments_entity];
|
|
comments_node_file = [comments_node];
|
|
}
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
iq.setTo(xid);
|
|
|
|
// Create the main XML nodes/childs
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB}));
|
|
var entry = item.appendChild(iq.buildNode('entry', {'xmlns': NS_ATOM}));
|
|
|
|
// Create the XML author childs
|
|
var author = entry.appendChild(iq.buildNode('author', {'xmlns': NS_ATOM}));
|
|
author.appendChild(iq.buildNode('name', {'xmlns': NS_ATOM}, author_nick));
|
|
author.appendChild(iq.buildNode('uri', {'xmlns': NS_ATOM}, 'xmpp:' + author_xid));
|
|
|
|
// Create the XML entry childs
|
|
entry.appendChild(iq.buildNode('content', {'type': 'text', 'xmlns': NS_ATOM}, body));
|
|
entry.appendChild(iq.buildNode('published', {'xmlns': NS_ATOM}, time));
|
|
entry.appendChild(iq.buildNode('updated', {'xmlns': NS_ATOM}, time));
|
|
entry.appendChild(iq.buildNode('link', {
|
|
'rel': 'alternate',
|
|
'href': 'xmpp:' + xid + '?;node=' + encodeURIComponent(NS_URN_MBLOG) + ';item=' + encodeURIComponent(id),
|
|
'xmlns': NS_ATOM
|
|
}));
|
|
|
|
// Create the attached files nodes
|
|
for(var i = 0; i < attachedurl.length; i++) {
|
|
// Not enough data?
|
|
if(!attachedurl[i])
|
|
continue;
|
|
|
|
// Append a new file element
|
|
var file = entry.appendChild(iq.buildNode('link', {'xmlns': NS_ATOM, 'rel': 'enclosure', 'href': attachedurl[i]}));
|
|
|
|
// Add attributes
|
|
if(attachedname[i])
|
|
file.setAttribute('title', attachedname[i]);
|
|
if(attachedtype[i])
|
|
file.setAttribute('type', attachedtype[i]);
|
|
if(attachedlength[i])
|
|
file.setAttribute('length', attachedlength[i]);
|
|
|
|
// Any thumbnail?
|
|
if(attachedthumb[i])
|
|
file.appendChild(iq.buildNode('link', {'xmlns': NS_URN_MBLOG, 'rel': 'self', 'title': 'thumb', 'type': attachedtype[i], 'href': attachedthumb[i]}));
|
|
|
|
// Any comments node?
|
|
if(!comments_entity_file[i] || !comments_node_file[i]) {
|
|
// Generate values
|
|
comments_entity_file[i] = HOST_PUBSUB;
|
|
comments_node_file[i] = NS_URN_MBLOG + ':comments/' + hex_md5(attachedurl[i] + attachedname[i] + attachedtype[i] + attachedlength[i] + time);
|
|
|
|
// Create the node
|
|
setupMicroblog(comments_entity_file[i], comments_node_file[i], '1', '1000000', 'open', 'open', true);
|
|
}
|
|
|
|
file.appendChild(iq.buildNode('link', {'xmlns': NS_URN_MBLOG, 'rel': 'replies', 'title': 'comments_file', 'href': 'xmpp:' + comments_entity_file[i] + '?;node=' + encodeURIComponent(comments_node_file[i])}));
|
|
}
|
|
|
|
// Create the comments child
|
|
entry.appendChild(iq.buildNode('link', {'xmlns': NS_ATOM, 'rel': 'replies', 'title': 'comments', 'href': 'xmpp:' + comments_entity + '?;node=' + encodeURIComponent(comments_node)}));
|
|
|
|
// Create the geoloc child
|
|
var geoloc_xml = getDB('geolocation', 'now');
|
|
|
|
if(geoloc_xml) {
|
|
// Create two position arrays
|
|
var geo_names = ['lat', 'lon', 'country', 'countrycode', 'region', 'postalcode', 'locality', 'street', 'building', 'text', 'uri', 'timestamp'];
|
|
var geo_values = parsePosition(XMLFromString(geoloc_xml));
|
|
|
|
// New geoloc child
|
|
var geoloc = entry.appendChild(iq.buildNode('geoloc', {'xmlns': NS_GEOLOC}));
|
|
|
|
// Append the geoloc content
|
|
for(var g = 0; g < geo_names.length; g++) {
|
|
if(geo_names[g] && geo_values[g])
|
|
geoloc.appendChild(iq.buildNode(geo_names[g], {'xmlns': NS_GEOLOC}, geo_values[g]));
|
|
}
|
|
}
|
|
|
|
// Send the IQ
|
|
con.send(iq, handleMyMicroblog);
|
|
|
|
// Create the XML comments PubSub nodes
|
|
if(node_create)
|
|
setupMicroblog(comments_entity, comments_node, '1', '1000000', 'open', 'open', true);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Attaches a file to a microblog post
|
|
function attachMicroblog() {
|
|
// File upload vars
|
|
var attach_options = {
|
|
dataType: 'xml',
|
|
beforeSubmit: waitMicroblogAttach,
|
|
success: handleMicroblogAttach
|
|
};
|
|
|
|
// Upload form submit event
|
|
$('#attach').submit(function() {
|
|
if(!exists('#attach .wait') && $('#attach input[type="file"]').val())
|
|
$(this).ajaxSubmit(attach_options);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Upload input change event
|
|
$('#attach input[type="file"]').change(function() {
|
|
if(!exists('#attach .wait') && $(this).val())
|
|
$('#attach').ajaxSubmit(attach_options);
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Unattaches a microblog file
|
|
function unattachMicroblog(id) {
|
|
// Individual removal?
|
|
if(id)
|
|
$('#attach .one-file[data-id="' + id + '"]').remove();
|
|
else
|
|
$('#attach .one-file').remove();
|
|
|
|
// Must enable the popup again?
|
|
if(!exists('#attach .one-file')) {
|
|
// Restore the bubble class
|
|
$('#attach').addClass('bubble');
|
|
|
|
// Enable the bubble click events
|
|
if(id) {
|
|
$('#attach').hide();
|
|
showBubble('#attach');
|
|
}
|
|
|
|
else
|
|
closeBubbles();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Wait event for file attaching
|
|
function waitMicroblogAttach() {
|
|
// Append the wait icon
|
|
$('#attach input[type="submit"]').after('<div class="wait wait-medium"></div>');
|
|
|
|
// Lock the bubble
|
|
$('#attach').removeClass('bubble');
|
|
}
|
|
|
|
// Success event for file attaching
|
|
function handleMicroblogAttach(responseXML) {
|
|
// Data selector
|
|
var dData = $(responseXML).find('jappix');
|
|
|
|
// Process the returned data
|
|
if(!dData.find('error').size()) {
|
|
// Do not allow this bubble to be hidden
|
|
$('#attach').removeClass('bubble');
|
|
|
|
// Get the file values
|
|
var fName = dData.find('title').text();
|
|
var fType = dData.find('type').text();
|
|
var fLength = dData.find('length').text();
|
|
var fURL = dData.find('href').text();
|
|
var fThumb = dData.find('thumb').text();
|
|
|
|
// Generate a file ID
|
|
var fID = hex_md5(fURL);
|
|
|
|
// Add this file
|
|
$('#attach .attach-subitem').append(
|
|
'<div class="one-file" data-type="' + encodeQuotes(fType) + '" data-length="' + encodeQuotes(fLength) + '" data-thumb="' + encodeQuotes(fThumb) + '" data-id="' + fID + '">' +
|
|
'<a class="remove talk-images" href="#" title="' + encodeQuotes(_e("Unattach the file")) + '"></a>' +
|
|
'<a class="link" href="' + encodeQuotes(fURL) + '" target="_blank">' + fName.htmlEnc() + '</a>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Click event
|
|
$('#attach .one-file[data-id="' + fID + '"] a.remove').click(function() {
|
|
return unattachMicroblog(fID);
|
|
});
|
|
|
|
logThis('File attached.', 3);
|
|
}
|
|
|
|
// Any error?
|
|
else {
|
|
openThisError(4);
|
|
|
|
// Unlock the bubble?
|
|
if(!exists('#attach .one-file')) {
|
|
$('#attach').addClass('bubble').hide();
|
|
|
|
// Show the bubble again!
|
|
showBubble('#attach');
|
|
}
|
|
|
|
logThis('Error while attaching the file: ' + dData.find('error').text(), 1);
|
|
}
|
|
|
|
// Reset the attach bubble
|
|
$('#attach input[type="file"]').val('');
|
|
$('#attach .wait').remove();
|
|
|
|
// Focus on the text input
|
|
$(document).oneTime(10, function() {
|
|
$('#channel .top input[name="microblog_body"]').focus();
|
|
});
|
|
}
|
|
|
|
// Shows the microblog of an user from his infos
|
|
function fromInfosMicroblog(xid, hash) {
|
|
// Renitialize the channel
|
|
resetMicroblog();
|
|
|
|
// Switch to the channel
|
|
switchChan('channel');
|
|
|
|
// Get the microblog
|
|
getMicroblog(xid, hash);
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchMicroblog() {
|
|
// Keyboard event
|
|
$('#channel .top input[name="microblog_body"]').keyup(function(e) {
|
|
// Enter pressed: send the microblog notice
|
|
if((e.keyCode == 13) && !exists('#attach .wait'))
|
|
return sendMicroblog();
|
|
})
|
|
|
|
// Placeholder
|
|
.placeholder();
|
|
|
|
// Microblog file attacher
|
|
attachMicroblog();
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the music JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 25/04/11
|
|
|
|
*/
|
|
|
|
// Opens the music bubble
|
|
function openMusic() {
|
|
var path = '.music-content';
|
|
|
|
// Show the music bubble
|
|
showBubble(path);
|
|
|
|
$(document).oneTime(10, function() {
|
|
$(path + ' input').focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Parses the music search XML
|
|
function parseMusic(xml, type) {
|
|
var path = '.music-content ';
|
|
var content = path + '.list';
|
|
var path_type = content + ' .' + type;
|
|
|
|
// Create the result container
|
|
if(!exists(path_type)) {
|
|
var code = '<div class="' + type + '"></div>';
|
|
|
|
if(type == 'local')
|
|
$(content).prepend(code);
|
|
else
|
|
$(content).append(code);
|
|
}
|
|
|
|
// Fill the results
|
|
$(xml).find('track').each(function() {
|
|
// Parse the XML
|
|
var id = $(this).find('id').text();
|
|
var title = $(this).find('name').text();
|
|
var artist = $(this).find('artist').text();
|
|
var source = $(this).find('source').text();
|
|
var duration = $(this).find('duration').text();
|
|
var uri = $(this).find('url').text();
|
|
var mime = $(this).find('type').text();
|
|
|
|
// No ID?
|
|
if(!id)
|
|
id = hex_md5(uri);
|
|
|
|
// No MIME?
|
|
if(!mime)
|
|
mime = 'audio/ogg';
|
|
|
|
// Local URL?
|
|
if(type == 'local')
|
|
uri = generateURL(uri);
|
|
|
|
// Append the HTML code
|
|
$(path_type).append('<a href="#" class="song" data-id="' + id + '">' + title + '</a>');
|
|
|
|
// Current playing song?
|
|
var current_song = $(path_type + ' a[data-id="' + id + '"]');
|
|
|
|
if(exists('.music-audio[data-id="' + id + '"]'))
|
|
current_song.addClass('playing');
|
|
|
|
// Click event
|
|
current_song.click(function() {
|
|
return addMusic(id, title, artist, source, duration, uri, mime, type);
|
|
});
|
|
});
|
|
|
|
// The search is finished
|
|
if(exists(content + ' .jamendo') && exists(content + ' .local')) {
|
|
// Get the result values
|
|
var jamendo = $(content + ' .jamendo').text();
|
|
var local = $(content + ' .local').text();
|
|
|
|
// Enable the input
|
|
$(path + 'input').val('').removeAttr('disabled');
|
|
|
|
// No result
|
|
if(!jamendo && !local)
|
|
$(path + '.no-results').show();
|
|
|
|
// We must put a separator between the categories
|
|
if(jamendo && local)
|
|
$(content + ' .local').addClass('special');
|
|
}
|
|
}
|
|
|
|
// Sends the music search requests
|
|
function searchMusic() {
|
|
var path = '.music-content ';
|
|
|
|
// We get the input string
|
|
var string = $(path + 'input').val();
|
|
|
|
// We lock the search input
|
|
$(path + 'input').attr('disabled', true);
|
|
|
|
// We reset the results
|
|
$(path + '.list div').remove();
|
|
$(path + '.no-results').hide();
|
|
|
|
// Get the Jamendo results
|
|
$.get('./php/music-search.php', {searchquery: string, location: 'jamendo'}, function(data) {
|
|
parseMusic(data, 'jamendo');
|
|
});
|
|
|
|
// Get the local results
|
|
$.get('./php/music-search.php', {searchquery: string, location: JAPPIX_LOCATION}, function(data) {
|
|
parseMusic(data, 'local');
|
|
});
|
|
}
|
|
|
|
// Performs an action on the music player
|
|
function actionMusic(action) {
|
|
try {
|
|
// Initialize
|
|
var playThis = document.getElementById('top-content').getElementsByTagName('audio')[0];
|
|
|
|
// Nothing to play, exit
|
|
if(!playThis)
|
|
return false;
|
|
|
|
var stopButton = $('#top-content a.stop');
|
|
|
|
// User play a song
|
|
if(action == 'play') {
|
|
stopButton.show();
|
|
playThis.load();
|
|
playThis.play();
|
|
playThis.addEventListener('ended', function() {
|
|
actionMusic('stop');
|
|
}, true);
|
|
|
|
logThis('Music is now playing.');
|
|
}
|
|
|
|
// User stop the song or the song came to its end
|
|
else if(action == 'stop') {
|
|
stopButton.hide();
|
|
playThis.pause();
|
|
$('#top-content .music').removeClass('actived');
|
|
$('.music-content .list a').removeClass('playing');
|
|
$('.music-audio').remove();
|
|
publishMusic();
|
|
|
|
logThis('Music is now stopped.');
|
|
}
|
|
}
|
|
|
|
catch(e) {}
|
|
|
|
finally {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Publishes the current title over PEP
|
|
function publishMusic(title, artist, source, duration, uri) {
|
|
// We share the tune on PEP if enabled
|
|
if(enabledPEP()) {
|
|
/* REF: http://xmpp.org/extensions/xep-0118.html */
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// Create the main PubSub nodes
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_TUNE, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB}));
|
|
var tune = item.appendChild(iq.buildNode('tune', {'xmlns': NS_TUNE}));
|
|
|
|
// Enough data?
|
|
if(title || artist || source || uri) {
|
|
// Data array
|
|
var nodes = new Array(
|
|
'title',
|
|
'artist',
|
|
'source',
|
|
'length',
|
|
'uri'
|
|
);
|
|
|
|
var values = new Array(
|
|
title,
|
|
artist,
|
|
source,
|
|
length,
|
|
uri
|
|
);
|
|
|
|
// Create the children nodes
|
|
for(i in nodes) {
|
|
if(values[i])
|
|
tune.appendChild(iq.buildNode(nodes[i], {'xmlns': NS_TUNE}, values[i]));
|
|
}
|
|
}
|
|
|
|
con.send(iq);
|
|
|
|
logThis('New tune sent: ' + title, 3);
|
|
}
|
|
}
|
|
|
|
// Adds a music title to the results
|
|
function addMusic(id, title, artist, source, duration, uri, mime, type) {
|
|
var path = '.music-content ';
|
|
|
|
// We remove & create a new audio tag
|
|
$('.music-audio').remove();
|
|
$(path + '.player').prepend('<audio class="music-audio" type="' + mime + '" data-id="' + id + '" />');
|
|
|
|
// We apply the new source to the player
|
|
if(type == 'jamendo')
|
|
$('.music-audio').attr('src', 'http://api.jamendo.com/get2/stream/track/redirect/?id=' + id + '&streamencoding=ogg2');
|
|
else
|
|
$('.music-audio').attr('src', uri);
|
|
|
|
// We play the target sound
|
|
actionMusic('play');
|
|
|
|
// We set the actived class
|
|
$('#top-content .music').addClass('actived');
|
|
|
|
// We set a current played track indicator
|
|
$(path + '.list a').removeClass('playing');
|
|
$(path + 'a[data-id="' + id + '"]').addClass('playing');
|
|
|
|
// We publish what we listen
|
|
publishMusic(title, artist, source, duration, uri);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchMusic() {
|
|
// When music search string submitted
|
|
$('.music-content input').keyup(function(e) {
|
|
// Enter : send
|
|
if(e.keyCode == 13 && $(this).val())
|
|
searchMusic();
|
|
|
|
// Escape : quit
|
|
if(e.keyCode == 27)
|
|
closeBubbles();
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the notification JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 04/05/12
|
|
|
|
*/
|
|
|
|
// Resets the notifications alert if no one remaining
|
|
function closeEmptyNotifications() {
|
|
if(!$('.one-notification').size())
|
|
closeBubbles();
|
|
}
|
|
|
|
// Checks if there are pending notifications
|
|
function checkNotifications() {
|
|
// Define the selectors
|
|
var notif = '#top-content .notifications';
|
|
var nothing = '.notifications-content .nothing';
|
|
var empty = '.notifications-content .empty';
|
|
|
|
// Get the notifications number
|
|
var number = $('.one-notification').size();
|
|
|
|
// Remove the red notify bubble
|
|
$(notif + ' .notify').remove();
|
|
|
|
// Any notification?
|
|
if(number) {
|
|
$(notif).prepend('<div class="notify one-counter" data-counter="' + number + '">' + number + '</div>');
|
|
$(nothing).hide();
|
|
$(empty).show();
|
|
}
|
|
|
|
// No notification!
|
|
else {
|
|
$(empty).hide();
|
|
$(nothing).show();
|
|
|
|
// Purge the social inbox node
|
|
purgeNotifications();
|
|
}
|
|
|
|
// Update the page title
|
|
updateTitle();
|
|
}
|
|
|
|
// Creates a new notification
|
|
function newNotification(type, from, data, body, id, inverse) {
|
|
if(!type || !from)
|
|
return;
|
|
|
|
// Generate an ID hash
|
|
if(!id)
|
|
var id = hex_md5(type + from);
|
|
|
|
// Generate the text to be displayed
|
|
var text, action, code;
|
|
var yes_path = 'href="#"';
|
|
|
|
// User things
|
|
from = bareXID(from);
|
|
var hash = hex_md5(from);
|
|
|
|
switch(type) {
|
|
case 'subscribe':
|
|
// Get the name to display
|
|
var display_name = data[1];
|
|
|
|
if(!display_name)
|
|
display_name = data[0];
|
|
|
|
text = '<b>' + display_name.htmlEnc() + '</b> ' + _e("would like to add you as a friend.") + ' ' + _e("Do you accept?");
|
|
|
|
break;
|
|
|
|
case 'invite_room':
|
|
text = '<b>' + getBuddyName(from).htmlEnc() + '</b> ' + _e("would like you to join this chatroom:") + ' <em>' + data[0].htmlEnc() + '</em> ' + _e("Do you accept?");
|
|
|
|
break;
|
|
|
|
case 'request':
|
|
text = '<b>' + from.htmlEnc() + '</b> ' + _e("would like to get authorization.") + ' ' + _e("Do you accept?");
|
|
|
|
break;
|
|
|
|
case 'send':
|
|
yes_path = 'href="' + encodeQuotes(data[1]) + '" target="_blank"';
|
|
|
|
text = '<b>' + getBuddyName(from).htmlEnc() + '</b> ' + printf(_e("would like to send you a file: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>') + ' ' + _e("Do you accept?");
|
|
|
|
break;
|
|
|
|
case 'send_pending':
|
|
text = '<b>' + getBuddyName(from).htmlEnc() + '</b> ' + printf(_e("has received a file exchange request: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'send_accept':
|
|
text = '<b>' + getBuddyName(from).htmlEnc() + '</b> ' + printf(_e("has accepted to receive your file: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'send_reject':
|
|
text = '<b>' + getBuddyName(from).htmlEnc() + '</b> ' + printf(_e("has rejected to receive your file: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'send_fail':
|
|
text = '<b>' + getBuddyName(from).htmlEnc() + '</b> ' + printf(_e("could not receive your file: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'rosterx':
|
|
text = printf(_e("Do you want to see the friends %s suggests you?").htmlEnc(), '<b>' + getBuddyName(from).htmlEnc() + '</b>');
|
|
|
|
break;
|
|
|
|
case 'comment':
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + printf(_e("commented an item you follow: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'like':
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + printf(_e("liked your post: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'quote':
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + printf(_e("quoted you somewhere: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'wall':
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + printf(_e("published on your wall: “%s”.").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'photo':
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + printf(_e("tagged you in a photo (%s).").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'video':
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + printf(_e("tagged you in a video (%s).").htmlEnc(), '<em>' + truncate(body, 25).htmlEnc() + '</em>');
|
|
|
|
break;
|
|
|
|
case 'me_profile_new_success':
|
|
yes_path = 'href="' + encodeQuotes(data[1]) + '" target="_blank"';
|
|
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + _e("validated your account. Your public profile will be available in a few moments.").htmlEnc();
|
|
|
|
break;
|
|
|
|
case 'me_profile_remove_success':
|
|
yes_path = 'href="' + encodeQuotes(data[1]) + '" target="_blank"';
|
|
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + _e("has removed your public profile after your request. We will miss you!").htmlEnc();
|
|
|
|
break;
|
|
|
|
case 'me_profile_update_success':
|
|
yes_path = 'href="' + encodeQuotes(data[1]) + '" target="_blank"';
|
|
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + _e("has saved your new public profile settings. They will be applied in a few moments.").htmlEnc();
|
|
|
|
break;
|
|
|
|
case 'me_profile_check_error':
|
|
yes_path = 'href="' + encodeQuotes(data[1]) + '" target="_blank"';
|
|
|
|
text = '<b>' + data[0].htmlEnc() + '</b> ' + _e("could not validate your account to create or update your public profile. Check your credentials.").htmlEnc();
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// No text?
|
|
if(!text)
|
|
return;
|
|
|
|
// Action links?
|
|
switch(type) {
|
|
// Hide/Show actions
|
|
case 'send_pending':
|
|
case 'send_accept':
|
|
case 'send_reject':
|
|
case 'send_fail':
|
|
case 'comment':
|
|
case 'like':
|
|
case 'quote':
|
|
case 'wall':
|
|
case 'photo':
|
|
case 'video':
|
|
action = '<a href="#" class="no">' + _e("Hide") + '</a>';
|
|
|
|
// Any parent link?
|
|
if((type == 'comment') && data[2])
|
|
action = '<a href="#" class="yes">' + _e("Show") + '</a>' + action;
|
|
|
|
break;
|
|
|
|
// Jappix Me actions
|
|
case 'me_profile_new_success':
|
|
case 'me_profile_remove_success':
|
|
case 'me_profile_update_success':
|
|
case 'me_profile_check_error':
|
|
action = '<a ' + yes_path + ' class="yes">' + _e("Open") + '</a><a href="#" class="no">' + _e("Hide") + '</a>';
|
|
|
|
break;
|
|
|
|
// Default actions
|
|
default:
|
|
action = '<a ' + yes_path + ' class="yes">' + _e("Yes") + '</a><a href="#" class="no">' + _e("No") + '</a>';
|
|
}
|
|
|
|
if(text) {
|
|
// We display the notification
|
|
if(!exists('.notifications-content .' + id)) {
|
|
// We create the html markup depending of the notification type
|
|
code = '<div class="one-notification ' + id + ' ' + hash + '" title="' + encodeQuotes(body) + ' - ' + _e("This notification is only informative, maybe the data it links to have been removed.") + '" data-type="' + encodeQuotes(type) + '">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
|
|
'<p class="notification-text">' + text + '</p>' +
|
|
'<p class="notification-actions">' +
|
|
'<span class="talk-images" />' +
|
|
action +
|
|
'</p>' +
|
|
'</div>';
|
|
|
|
// Add the HTML code
|
|
if(inverse)
|
|
$('.notifications-content .nothing').before(code);
|
|
else
|
|
$('.notifications-content .empty').after(code);
|
|
|
|
// Play a sound to alert the user
|
|
soundPlay(2);
|
|
|
|
// The yes click function
|
|
$('.' + id + ' a.yes').click(function() {
|
|
actionNotification(type, data, 'yes', id);
|
|
|
|
if(($(this).attr('href') == '#') && ($(this).attr('target') != '_blank'))
|
|
return false;
|
|
});
|
|
|
|
// The no click function
|
|
$('.' + id + ' a.no').click(function() {
|
|
return actionNotification(type, data, 'no', id);
|
|
});
|
|
|
|
// Get the user avatar
|
|
getAvatar(from, 'cache', 'true', 'forget');
|
|
}
|
|
}
|
|
|
|
// We tell the user he has a new pending notification
|
|
checkNotifications();
|
|
|
|
logThis('New notification: ' + from, 3);
|
|
}
|
|
|
|
// Performs an action on a given notification
|
|
function actionNotification(type, data, value, id) {
|
|
// We launch a function depending of the type
|
|
if((type == 'subscribe') && (value == 'yes'))
|
|
acceptSubscribe(data[0], data[1]);
|
|
|
|
else if((type == 'subscribe') && (value == 'no'))
|
|
sendSubscribe(data[0], 'unsubscribed');
|
|
|
|
else if((type == 'invite_room') && (value == 'yes'))
|
|
checkChatCreate(data[0], 'groupchat');
|
|
|
|
else if(type == 'request')
|
|
requestReply(value, data[0]);
|
|
|
|
if((type == 'send') && (value == 'yes'))
|
|
replyOOB(data[0], data[3], 'accept', data[2], data[4]);
|
|
|
|
else if((type == 'send') && (value == 'no'))
|
|
replyOOB(data[0], data[3], 'reject', data[2], data[4]);
|
|
|
|
else if((type == 'rosterx') && (value == 'yes'))
|
|
openRosterX(data[0]);
|
|
|
|
else if((type == 'comment') || (type == 'like') || (type == 'quote') || (type == 'wall') || (type == 'photo') || (type == 'video')) {
|
|
if(value == 'yes') {
|
|
// Get the microblog item
|
|
fromInfosMicroblog(data[2]);
|
|
|
|
// Append the marker
|
|
$('#channel .top.individual').append('<input type="hidden" name="comments" value="' + encodeQuotes(data[1]) + '" />');
|
|
}
|
|
|
|
removeNotification(data[3]);
|
|
}
|
|
|
|
// We remove the notification
|
|
$('.notifications-content .' + id).remove();
|
|
|
|
// We check if there's any other pending notification
|
|
closeEmptyNotifications();
|
|
checkNotifications();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Clear the social notifications
|
|
function clearNotifications() {
|
|
// Remove notifications
|
|
$('.one-notification').remove();
|
|
|
|
// Refresh
|
|
closeEmptyNotifications();
|
|
checkNotifications();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the pending social notifications
|
|
function getNotifications() {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
pubsub.appendChild(iq.buildNode('items', {'node': NS_URN_INBOX, 'xmlns': NS_PUBSUB}));
|
|
|
|
con.send(iq, handleNotifications);
|
|
|
|
logThis('Getting social notifications...');
|
|
}
|
|
|
|
// Handles the social notifications
|
|
function handleNotifications(iq) {
|
|
// Any error?
|
|
if((iq.getType() == 'error') && $(iq.getNode()).find('item-not-found').size()) {
|
|
// The node may not exist, create it!
|
|
setupMicroblog('', NS_URN_INBOX, '1', '1000000', 'whitelist', 'open', true);
|
|
|
|
logThis('Error while getting social notifications, trying to reconfigure the Pubsub node!', 2);
|
|
}
|
|
|
|
// Selector
|
|
var items = $(iq.getNode()).find('item');
|
|
|
|
// Should we inverse?
|
|
var inverse = true;
|
|
|
|
if(items.size() == 1)
|
|
inverse = false;
|
|
|
|
// Parse notifications
|
|
items.each(function() {
|
|
// Parse the current item
|
|
var current_item = $(this).attr('id');
|
|
var current_type = $(this).find('link[rel="via"]:first').attr('title');
|
|
var current_href = $(this).find('link[rel="via"]:first').attr('href');
|
|
var current_parent_href = $(this).find('link[rel="related"]:first').attr('href');
|
|
var current_xid = explodeThis(':', $(this).find('author uri').text(), 1);
|
|
var current_name = $(this).find('author name').text();
|
|
var current_text = $(this).find('content[type="text"]:first').text();
|
|
var current_bname = getBuddyName(current_xid);
|
|
var current_id = hex_md5(current_type + current_xid + current_href + current_text);
|
|
|
|
// Choose the good name!
|
|
if(!current_name || (current_bname != getXIDNick(current_xid)))
|
|
current_name = current_bname;
|
|
|
|
// Create it!
|
|
newNotification(current_type, current_xid, [current_name, current_href, current_parent_href, current_item], current_text, current_id, inverse);
|
|
});
|
|
|
|
logThis(items.size() + ' social notification(s) got!', 3);
|
|
}
|
|
|
|
// Sends a social notification
|
|
function sendNotification(xid, type, href, text, parent) {
|
|
// Notification ID
|
|
var id = hex_md5(xid + text + getTimeStamp());
|
|
|
|
// IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
iq.setTo(xid);
|
|
|
|
// ATOM content
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_URN_INBOX, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB}));
|
|
var entry = item.appendChild(iq.buildNode('entry', {'xmlns': NS_ATOM}));
|
|
|
|
// Notification author (us)
|
|
var author = entry.appendChild(iq.buildNode('author', {'xmlns': NS_ATOM}));
|
|
author.appendChild(iq.buildNode('name', {'xmlns': NS_ATOM}, getName()));
|
|
author.appendChild(iq.buildNode('uri', {'xmlns': NS_ATOM}, 'xmpp:' + getXID()));
|
|
|
|
// Notification content
|
|
entry.appendChild(iq.buildNode('published', {'xmlns': NS_ATOM}, getXMPPTime('utc')));
|
|
entry.appendChild(iq.buildNode('content', {'type': 'text', 'xmlns': NS_ATOM}, text));
|
|
entry.appendChild(iq.buildNode('link', {'rel': 'via', 'title': type, 'href': href, 'xmlns': NS_ATOM}));
|
|
|
|
// Any parent item?
|
|
if(parent && parent[0] && parent[1] && parent[2]) {
|
|
// Generate the parent XMPP URI
|
|
var parent_href = 'xmpp:' + parent[0] + '?;node=' + encodeURIComponent(parent[1]) + ';item=' + encodeURIComponent(parent[2]);
|
|
|
|
entry.appendChild(iq.buildNode('link', {'rel': 'related', 'href': parent_href, 'xmlns': NS_ATOM}));
|
|
}
|
|
|
|
con.send(iq);
|
|
|
|
logThis('Sending a social notification to ' + xid + ' (type: ' + type + ')...');
|
|
}
|
|
|
|
// Removes a social notification
|
|
function removeNotification(id) {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var retract = pubsub.appendChild(iq.buildNode('retract', {'node': NS_URN_INBOX, 'xmlns': NS_PUBSUB}));
|
|
retract.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB}));
|
|
|
|
con.send(iq);
|
|
}
|
|
|
|
// Purge the social notifications
|
|
function purgeNotifications() {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB_OWNER});
|
|
pubsub.appendChild(iq.buildNode('purge', {'node': NS_URN_INBOX, 'xmlns': NS_PUBSUB_OWNER}));
|
|
|
|
con.send(iq);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Adapt the notifications bubble max-height
|
|
function adaptNotifications() {
|
|
// Process the new height
|
|
var max_height = $('#right-content').height() - 22;
|
|
|
|
// New height too small
|
|
if(max_height < 250)
|
|
max_height = 250;
|
|
|
|
// Apply the new height
|
|
$('.notifications-content .tools-content-subitem').css('max-height', max_height);
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchNotifications() {
|
|
// Adapt the notifications height
|
|
adaptNotifications();
|
|
}
|
|
|
|
// Window resize event handler
|
|
$(window).resize(adaptNotifications);
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the http-reply JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 16/04/13
|
|
|
|
*/
|
|
|
|
// Replies to a HTTP request
|
|
function requestReply(value, xml) {
|
|
// We parse the xml content
|
|
var from = fullXID(getStanzaFrom(xml));
|
|
var confirm = $(xml.getNode()).find('confirm');
|
|
var xmlns = confirm.attr('xmlns');
|
|
var id = confirm.attr('id');
|
|
var method = confirm.attr('method');
|
|
var url = confirm.attr('url');
|
|
|
|
// We generate the reply message
|
|
var aMsg = new JSJaCMessage();
|
|
aMsg.setTo(from);
|
|
|
|
// If "no"
|
|
if(value == 'no') {
|
|
aMsg.setType('error');
|
|
aMsg.appendNode('error', {'code': '401', 'type': 'auth'});
|
|
}
|
|
|
|
// We set the confirm node
|
|
aMsg.appendNode('confirm', {'xmlns': xmlns, 'url': url, 'id': id, 'method': method});
|
|
|
|
// We send the message
|
|
con.send(aMsg, handleErrorReply);
|
|
|
|
logThis('Replying HTTP auth request: ' + from, 3);
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the options JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 07/06/13
|
|
|
|
*/
|
|
|
|
// Opens the options popup
|
|
function optionsOpen() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Edit options") + '</div>' +
|
|
|
|
'<div class="tab">' +
|
|
'<a href="#" class="tab-general tab-active" data-key="1">' + _e("General") + '</a>' +
|
|
'<a href="#" class="tab-channel pubsub-hidable pubsub-hidable-cn" data-key="2">' + _e("Channel") + '</a>' +
|
|
'<a href="#" class="tab-account" data-key="3">' + _e("Account") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div id="conf1" class="lap-active one-lap forms">' +
|
|
'<fieldset class="privacy">' +
|
|
'<legend>' + _e("Privacy") + '</legend>' +
|
|
|
|
'<div class="geolocation">' +
|
|
'<label for="geolocation" class="pep-hidable">' + _e("Geolocation") + '</label>' +
|
|
'<input id="geolocation" type="checkbox" class="pep-hidable" />' +
|
|
'</div>' +
|
|
|
|
'<div class="archiving">' +
|
|
'<label for="archiving" class="mam-hidable">' + _e("Message archiving") + '</label>' +
|
|
'<select id="archiving" class="mam-hidable">' +
|
|
'<option value="never">' + _e("Disabled") + '</option>' +
|
|
'<option value="roster">' + _e("Store friend chats") + '</option>' +
|
|
'<option value="always">' + _e("Store all chats") + '</option>' +
|
|
'</select>' +
|
|
'<a href="#" class="linked empty-archives mam-hidable">' + _e("Remove all archives") + '</a>' +
|
|
'</div>' +
|
|
'</fieldset>' +
|
|
|
|
'<fieldset class="application">' +
|
|
'<legend>' + _e("Application") + '</legend>' +
|
|
|
|
'<div class="sounds">' +
|
|
'<label for="sounds">' + _e("Sounds") + '</label>' +
|
|
'<input id="sounds" type="checkbox" />' +
|
|
'</div>' +
|
|
|
|
'<div class="showall">' +
|
|
'<label for="showall">' + _e("Show all friends") + '</label>' +
|
|
'<input id="showall" type="checkbox" />' +
|
|
'</div>' +
|
|
|
|
'<div class="integratemedias">' +
|
|
'<label for="integratemedias">' + _e("Media integration") + '</label>' +
|
|
'<input id="integratemedias" type="checkbox" />' +
|
|
'</div>' +
|
|
|
|
'<div class="xmpplinks">' +
|
|
'<label class="xmpplinks-hidable">' + _e("XMPP links") + '</label>' +
|
|
'<a href="#" class="linked xmpp-links xmpplinks-hidable">' + _e("Open XMPP links with Jappix") + '</a>' +
|
|
'</div>' +
|
|
'</fieldset>' +
|
|
|
|
'<div class="sub-ask sub-ask-mam sub-ask-element">' +
|
|
'<div class="sub-ask-top">' +
|
|
'<div class="sub-ask-title">' + _e("Remove all archives") + '</div>' +
|
|
'<a href="#" class="sub-ask-close">X</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="sub-ask-content">' +
|
|
'<label>' + _e("Password") + '</label>' +
|
|
'<input type="password" class="purge-archives check-mam" required="" />' +
|
|
'</div>' +
|
|
|
|
'<a href="#" class="sub-ask-bottom">' + _e("Remove") + ' »</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div id="conf2" class="one-lap forms">' +
|
|
'<fieldset class="channel">' +
|
|
'<legend>' + _e("Channel") + '</legend>' +
|
|
|
|
'<div class="empty-channel">' +
|
|
'<label>' + _e("Empty") + '</label>' +
|
|
'<a href="#" class="linked empty-channel">' + _e("Empty channel") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="persistent">' +
|
|
'<label>' + _e("Persistent") + '</label>' +
|
|
'<input id="persistent" type="checkbox" />' +
|
|
'</div>' +
|
|
|
|
'<div class="maxnotices">' +
|
|
'<label>' + _e("Maximum notices") + '</label>' +
|
|
'<select id="maxnotices">' +
|
|
'<option value="1">1</option>' +
|
|
'<option value="100">100</option>' +
|
|
'<option value="1000">1000</option>' +
|
|
'<option value="10000">10000</option>' +
|
|
'<option value="100000">100000</option>' +
|
|
'<option value="1000000">1000000</option>' +
|
|
'</select>' +
|
|
'</div>' +
|
|
|
|
'</fieldset>' +
|
|
|
|
'<div class="sub-ask sub-ask-empty sub-ask-element">' +
|
|
'<div class="sub-ask-top">' +
|
|
'<div class="sub-ask-title">' + _e("Empty channel") + '</div>' +
|
|
'<a href="#" class="sub-ask-close">X</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="sub-ask-content">' +
|
|
'<label>' + _e("Password") + '</label>' +
|
|
'<input type="password" class="purge-microblog check-empty" required="" />' +
|
|
'</div>' +
|
|
|
|
'<a href="#" class="sub-ask-bottom">' + _e("Empty") + ' »</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div id="conf3" class="one-lap forms">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Account") + '</legend>' +
|
|
|
|
'<label>' + _e("Password") + '</label>' +
|
|
'<a href="#" class="linked change-password">' + _e("Change password") + '</a>' +
|
|
|
|
'<label>' + _e("Delete") + '</label>' +
|
|
'<a href="#" class="linked delete-account">' + _e("Delete account") + '</a>' +
|
|
'</fieldset>' +
|
|
|
|
'<div class="sub-ask sub-ask-pass sub-ask-element">' +
|
|
'<div class="sub-ask-top">' +
|
|
'<div class="sub-ask-title">' + _e("Change password") + '</div>' +
|
|
'<a href="#" class="sub-ask-close">X</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="sub-ask-content">' +
|
|
'<label>' + _e("Old") + '</label>' +
|
|
'<input type="password" class="password-change old" required="" />' +
|
|
|
|
'<label>' + _e("New (2 times)") + '</label>' +
|
|
'<input type="password" class="password-change new1" required="" />' +
|
|
'<input type="password" class="password-change new2" required="" />' +
|
|
'</div>' +
|
|
|
|
'<a href="#" class="sub-ask-bottom">' + _e("Continue") + ' »</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="sub-ask sub-ask-delete sub-ask-element">' +
|
|
'<div class="sub-ask-top">' +
|
|
'<div class="sub-ask-title">' + _e("Delete account") + '</div>' +
|
|
'<a href="#" class="sub-ask-close">X</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="sub-ask-content">' +
|
|
'<label>' + _e("Password") + '</label>' +
|
|
'<input type="password" class="delete-account check-password" required="" />' +
|
|
'</div>' +
|
|
|
|
'<a href="#" class="sub-ask-bottom">' + _e("Delete") + ' »</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish save">' + _e("Save") + '</a>' +
|
|
'<a href="#" class="finish cancel">' + _e("Cancel") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('options', html);
|
|
|
|
// Apply the features
|
|
applyFeatures('options');
|
|
|
|
// Associate the events
|
|
launchOptions();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Closes the options popup
|
|
function closeOptions() {
|
|
// Destroy the popup
|
|
destroyPopup('options');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks whether the options are loaded or not
|
|
function loadedOptions() {
|
|
if($('.options-hidable').is(':visible'))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Switches between the options tabs
|
|
function switchOptions(id) {
|
|
$('#options .one-lap').hide();
|
|
$('#options #conf' + id).show();
|
|
$('#options .tab a').removeClass('tab-active');
|
|
$('#options .tab a[data-key="' + id + '"]').addClass('tab-active');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Manages the options wait item
|
|
function waitOptions(id) {
|
|
var sOptions = $('#options .content');
|
|
|
|
// Remove the current item class
|
|
sOptions.removeClass(id);
|
|
|
|
// Hide the waiting items if all was received
|
|
if(!sOptions.hasClass('microblog') && !sOptions.hasClass('mam')) {
|
|
$('#options .wait').hide();
|
|
$('#options .finish:first').removeClass('disabled');
|
|
}
|
|
}
|
|
|
|
// Sends the options to the XMPP server
|
|
function storeOptions() {
|
|
// Get the values
|
|
var sounds = getDB('options', 'sounds');
|
|
var geolocation = getDB('options', 'geolocation');
|
|
var showall = getDB('options', 'roster-showall');
|
|
var integratemedias = getDB('options', 'integratemedias');
|
|
var status = getDB('options', 'presence-status');
|
|
|
|
// Create an array to be looped
|
|
var oType = new Array('sounds', 'geolocation', 'roster-showall', 'integratemedias', 'presence-status');
|
|
var oContent = new Array(sounds, geolocation, showall, integratemedias, status);
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var query = iq.setQuery(NS_PRIVATE);
|
|
var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_OPTIONS}));
|
|
|
|
// Loop the array
|
|
for(i in oType)
|
|
storage.appendChild(iq.buildNode('option', {'type': oType[i], 'xmlns': NS_OPTIONS}, oContent[i]));
|
|
|
|
con.send(iq, handleStoreOptions);
|
|
|
|
logThis('Storing options...', 3);
|
|
}
|
|
|
|
// Handles the option storing
|
|
function handleStoreOptions(iq) {
|
|
if(!iq || (iq.getType() != 'result'))
|
|
logThis('Options not stored.', 2);
|
|
else
|
|
logThis('Options stored.', 3);
|
|
}
|
|
|
|
// Saves the user options
|
|
function saveOptions() {
|
|
// We apply the sounds
|
|
var sounds = '0';
|
|
|
|
if($('#sounds').filter(':checked').size())
|
|
sounds = '1';
|
|
|
|
setDB('options', 'sounds', sounds);
|
|
|
|
// We apply the geolocation
|
|
if($('#geolocation').filter(':checked').size()) {
|
|
setDB('options', 'geolocation', '1');
|
|
|
|
// We geolocate the user on the go
|
|
geolocate();
|
|
}
|
|
|
|
else {
|
|
setDB('options', 'geolocation', '0');
|
|
|
|
// We delete the geolocation informations
|
|
sendPosition();
|
|
removeDB('geolocation', 'now');
|
|
}
|
|
|
|
// We apply the roster show all
|
|
if($('#showall').filter(':checked').size()) {
|
|
setDB('options', 'roster-showall', '1');
|
|
showAllBuddies('options');
|
|
}
|
|
|
|
else {
|
|
setDB('options', 'roster-showall', '0');
|
|
showOnlineBuddies('options');
|
|
}
|
|
|
|
// We apply the media integration
|
|
var integratemedias = '0';
|
|
|
|
if($('#integratemedias').filter(':checked').size())
|
|
integratemedias = '1';
|
|
|
|
setDB('options', 'integratemedias', integratemedias);
|
|
|
|
// We apply the message archiving
|
|
if(enabledMAM()) {
|
|
setConfigMAM($('#archiving').val() || 'never');
|
|
}
|
|
|
|
// We apply the microblog configuration
|
|
var persist = '0';
|
|
var maximum = $('#maxnotices').val();
|
|
|
|
if($('#persistent').filter(':checked').size())
|
|
persist = '1';
|
|
|
|
if(enabledPEP() && (enabledPubSub() || enabledPubSubCN()))
|
|
setupMicroblog('', NS_URN_MBLOG, persist, maximum, '', '', false);
|
|
|
|
// We send the options to the database
|
|
storeOptions();
|
|
|
|
// Close the options
|
|
closeOptions();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the password changing
|
|
function handlePwdChange(iq) {
|
|
// Remove the general wait item
|
|
removeGeneralWait();
|
|
|
|
// If no errors
|
|
if(!handleErrorReply(iq)) {
|
|
clearLastSession();
|
|
quit();
|
|
openThisInfo(1);
|
|
|
|
logThis('Password changed.', 3);
|
|
}
|
|
|
|
else
|
|
logThis('Password not changed.', 2);
|
|
}
|
|
|
|
// Sends the new account password
|
|
function sendNewPassword() {
|
|
/* REF: http://xmpp.org/extensions/xep-0077.html#usecases-changepw */
|
|
|
|
var password0 = $('#options .old').val();
|
|
var password1 = $('#options .new1').val();
|
|
var password2 = $('#options .new2').val();
|
|
|
|
if ((password1 == password2) && (password0 == getPassword())) {
|
|
// We show the waiting image
|
|
showGeneralWait();
|
|
|
|
// We send the IQ
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setTo(getServer());
|
|
iq.setType('set');
|
|
|
|
var iqQuery = iq.setQuery(NS_REGISTER);
|
|
|
|
iqQuery.appendChild(iq.buildNode('username', {'xmlns': NS_REGISTER}, con.username));
|
|
iqQuery.appendChild(iq.buildNode('password', {'xmlns': NS_REGISTER}, password1));
|
|
|
|
con.send(iq, handlePwdChange);
|
|
|
|
logThis('Password change sent.', 3);
|
|
} else {
|
|
$('.sub-ask-pass input').each(function() {
|
|
var select = $(this);
|
|
|
|
if(!select.val())
|
|
$(document).oneTime(10, function() {
|
|
select.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
select.removeClass('please-complete');
|
|
});
|
|
|
|
if(password0 != getPassword())
|
|
$(document).oneTime(10, function() {
|
|
$('#options .old').addClass('please-complete').focus();
|
|
});
|
|
if(password1 != password2)
|
|
$(document).oneTime(10, function() {
|
|
$('#options .new1, #options .new2').addClass('please-complete').focus();
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the account deletion request
|
|
function handleAccDeletion(iq) {
|
|
// Remove the general wait item
|
|
removeGeneralWait();
|
|
|
|
// If no errors
|
|
if(!handleErrorReply(iq)) {
|
|
clearLastSession();
|
|
destroyTalkPage();
|
|
openThisInfo(2);
|
|
logout();
|
|
|
|
logThis('Account deleted.', 3);
|
|
} else {
|
|
logThis('Account not deleted.', 2);
|
|
}
|
|
}
|
|
|
|
// Purge the user's archives (MAM)
|
|
function purgeMyArchives() {
|
|
var password = $('#options .check-mam').val();
|
|
|
|
if(password == getPassword()) {
|
|
purgeArchivesMAM();
|
|
|
|
// Clear archives in UI
|
|
$('.page-engine-chan[data-type="chat"] .tools-clear').click();
|
|
|
|
// Hide the tool
|
|
$('#options .sub-ask').hide();
|
|
} else {
|
|
var selector = $('#options .check-mam');
|
|
|
|
if(password != getPassword())
|
|
$(document).oneTime(10, function() {
|
|
selector.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
selector.removeClass('please-complete');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Purge the user's microblog items
|
|
function purgeMyMicroblog() {
|
|
/* REF: http://xmpp.org/extensions/xep-0060.html#owner-purge */
|
|
|
|
var password = $('#options .check-empty').val();
|
|
|
|
if(password == getPassword()) {
|
|
// Send the IQ to remove the item (and get eventual error callback)
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB_OWNER});
|
|
pubsub.appendChild(iq.buildNode('purge', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB_OWNER}));
|
|
|
|
con.send(iq, handleMicroblogPurge);
|
|
|
|
// Hide the tool
|
|
$('#options .sub-ask').hide();
|
|
|
|
logThis('Microblog purge sent.', 3);
|
|
} else {
|
|
var selector = $('#options .check-empty');
|
|
|
|
if(password != getPassword())
|
|
$(document).oneTime(10, function() {
|
|
selector.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
selector.removeClass('please-complete');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handles the microblog purge
|
|
function handleMicroblogPurge(iq) {
|
|
// If no errors
|
|
if(!handleErrorReply(iq)) {
|
|
// Remove the microblog items
|
|
$('.one-update.update_' + hex_md5(getXID())).remove();
|
|
|
|
logThis('Microblog purged.', 3);
|
|
}
|
|
|
|
else
|
|
logThis('Microblog not purged.', 2);
|
|
}
|
|
|
|
// Deletes the user's account
|
|
function deleteMyAccount() {
|
|
/* REF: http://xmpp.org/extensions/xep-0077.html#usecases-cancel */
|
|
|
|
var password = $('#options .check-password').val();
|
|
|
|
if(password == getPassword()) {
|
|
// We show the waiting image
|
|
showGeneralWait();
|
|
|
|
// We send the IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var iqQuery = iq.setQuery(NS_REGISTER);
|
|
iqQuery.appendChild(iq.buildNode('remove', {'xmlns': NS_REGISTER}));
|
|
|
|
con.send(iq, handleAccDeletion);
|
|
|
|
logThis('Delete account sent.', 3);
|
|
}
|
|
|
|
else {
|
|
var selector = $('#options .check-password');
|
|
|
|
if(password != getPassword())
|
|
$(document).oneTime(10, function() {
|
|
selector.addClass('please-complete').focus();
|
|
});
|
|
else
|
|
selector.removeClass('please-complete');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Loads the user options
|
|
function loadOptions() {
|
|
// Process the good stuffs, depending of the server features
|
|
var enabled_mam = enabledMAM();
|
|
var enabled_pubsub = enabledPubSub();
|
|
var enabled_pubsub_cn = enabledPubSubCN();
|
|
var enabled_pep = enabledPEP();
|
|
var sWait = $('#options .content');
|
|
|
|
// Show the waiting items if necessary
|
|
if(enabled_mam || (enabled_pep && (enabled_pubsub || enabled_pubsub_cn))) {
|
|
$('#options .wait').show();
|
|
$('#options .finish:first').addClass('disabled');
|
|
}
|
|
|
|
// We get the archiving configuration
|
|
if(enabled_mam) {
|
|
sWait.addClass('mam');
|
|
getConfigMAM();
|
|
}
|
|
|
|
// We get the microblog configuration
|
|
if((enabled_pubsub || enabled_pubsub_cn) && enabled_pep) {
|
|
sWait.addClass('microblog');
|
|
getConfigMicroblog();
|
|
}
|
|
|
|
// We show the "privacy" form if something is visible into it
|
|
if(enabled_mam || enabled_pep)
|
|
$('#options fieldset.privacy').show();
|
|
|
|
// We get the values of the forms for the sounds
|
|
if(getDB('options', 'sounds') == '0')
|
|
$('#sounds').attr('checked', false);
|
|
else
|
|
$('#sounds').attr('checked', true);
|
|
|
|
// We get the values of the forms for the geolocation
|
|
if(getDB('options', 'geolocation') == '1')
|
|
$('#geolocation').attr('checked', true);
|
|
else
|
|
$('#geolocation').attr('checked', false);
|
|
|
|
// We get the values of the forms for the roster show all
|
|
if(getDB('options', 'roster-showall') == '1')
|
|
$('#showall').attr('checked', true);
|
|
else
|
|
$('#showall').attr('checked', false);
|
|
|
|
// We get the values of the forms for the integratemedias
|
|
if(getDB('options', 'integratemedias') == '0')
|
|
$('#integratemedias').attr('checked', false);
|
|
else
|
|
$('#integratemedias').attr('checked', true);
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchOptions() {
|
|
// Click events
|
|
$('#options .tab a').click(function() {
|
|
// Yet active?
|
|
if($(this).hasClass('tab-active'))
|
|
return false;
|
|
|
|
// Switch to the good tab
|
|
var key = parseInt($(this).attr('data-key'));
|
|
|
|
return switchOptions(key);
|
|
});
|
|
|
|
$('#options .linked').click(function() {
|
|
$('#options .sub-ask').hide();
|
|
});
|
|
|
|
$('#options .xmpp-links').click(function() {
|
|
xmppLinksHandler();
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#options .empty-archives').click(function() {
|
|
var selector = '#options .sub-ask-mam';
|
|
|
|
$(selector).show();
|
|
|
|
$(document).oneTime(10, function() {
|
|
$(selector + ' input').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#options .empty-channel').click(function() {
|
|
var selector = '#options .sub-ask-empty';
|
|
|
|
$(selector).show();
|
|
|
|
$(document).oneTime(10, function() {
|
|
$(selector + ' input').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#options .change-password').click(function() {
|
|
var selector = '#options .sub-ask-pass';
|
|
|
|
$(selector).show();
|
|
|
|
$(document).oneTime(10, function() {
|
|
$(selector + ' input:first').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#options .delete-account').click(function() {
|
|
var selector = '#options .sub-ask-delete';
|
|
|
|
$(selector).show();
|
|
|
|
$(document).oneTime(10, function() {
|
|
$(selector + ' input').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#options .sub-ask-pass .sub-ask-bottom').click(function() {
|
|
return sendNewPassword();
|
|
});
|
|
|
|
$('#options .sub-ask-mam .sub-ask-bottom').click(function() {
|
|
return purgeMyArchives();
|
|
});
|
|
|
|
$('#options .sub-ask-empty .sub-ask-bottom').click(function() {
|
|
return purgeMyMicroblog();
|
|
});
|
|
|
|
$('#options .sub-ask-delete .sub-ask-bottom').click(function() {
|
|
return deleteMyAccount();
|
|
});
|
|
|
|
$('#options .sub-ask-close').click(function() {
|
|
$('#options .sub-ask').hide();
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#options .bottom .finish').click(function() {
|
|
if($(this).is('.save') && !$(this).hasClass('disabled'))
|
|
return saveOptions();
|
|
if($(this).is('.cancel'))
|
|
return closeOptions();
|
|
|
|
return false;
|
|
});
|
|
|
|
// The keyup events
|
|
$('#options .sub-ask input').keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// Archives purge
|
|
if($(this).is('.purge-archives'))
|
|
return purgeMyArchives();
|
|
|
|
// Microblog purge
|
|
else if($(this).is('.purge-microblog'))
|
|
return purgeMyMicroblog();
|
|
|
|
// Password change
|
|
else if($(this).is('.password-change'))
|
|
return sendNewPassword();
|
|
|
|
// Account deletion
|
|
else if($(this).is('.delete-account'))
|
|
return deleteMyAccount();
|
|
}
|
|
});
|
|
|
|
// Load the options
|
|
loadOptions();
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the integratebox JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 03/12/11
|
|
|
|
*/
|
|
|
|
// Opens the integratebox popup
|
|
function openIntegrateBox() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Media viewer") + '</div>' +
|
|
|
|
'<div class="content"></div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish close">' + _e("Close") + '</a>' +
|
|
'<a href="#" class="finish next disabled" title="' + _e("Next") + '">></a>' +
|
|
'<a href="#" class="finish previous disabled" title="' + _e("Previous") + '"><</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('integratebox', html);
|
|
|
|
// Associate the events
|
|
launchIntegratebox();
|
|
}
|
|
|
|
// Closes the integratebox popup
|
|
function closeIntegrateBox() {
|
|
// Destroy the popup
|
|
destroyPopup('integratebox');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Generates the integratebox HTML code
|
|
function codeIntegrateBox(serv, url) {
|
|
var code = '';
|
|
|
|
// Protocol to use
|
|
var protocol = 'http';
|
|
|
|
if(isHTTPS())
|
|
protocol = 'https';
|
|
|
|
// Legacy browser
|
|
var legacy = false;
|
|
|
|
if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9))
|
|
legacy = true;
|
|
|
|
// Switch to get the good DOM code
|
|
switch(serv) {
|
|
case 'youtube':
|
|
if(legacy)
|
|
code = '<object width="640" height="385"><param name="movie" value="http://www.youtube.com/v/' + url + '&autoplay=1"></param><embed src="http://www.youtube.com/v/' + encodeQuotes(url) + '&autoplay=1" type="application/x-shockwave-flash" width="640" height="385"></embed></object>';
|
|
else
|
|
code = '<object width="640" height="385" data="' + encodeQuotes(protocol) + '://www.youtube.com/embed/' + encodeQuotes(url) + '?autoplay=1" type="text/html"><a href="http://www.youtube.com/watch?v=' + encodeQuotes(url) + '" target="_blank">http://www.youtube.com/watch?v=' + encodeQuotes(url) + '</a></object>';
|
|
|
|
break;
|
|
|
|
case 'dailymotion':
|
|
code = '<object width="640" height="385"><param name="movie" value="http://www.dailymotion.com/swf/video/' + url + '&autoplay=1"></param><param name="allowFullScreen" value="false"></param><embed type="application/x-shockwave-flash" src="http://www.dailymotion.com/swf/video/' + encodeQuotes(url) + '&autoplay=1" width="640" height="385" allowfullscreen="true" allowscriptaccess="always"></embed></object>';
|
|
|
|
break;
|
|
|
|
case 'vimeo':
|
|
code = '<object width="640" height="385"><param name="allowfullscreen" value="true" /><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=' + encodeQuotes(url) + '&server=vimeo.com&show_title=1&show_byline=1&show_portrait=0&color=&fullscreen=1&autoplay=1" /><embed src="http://vimeo.com/moogaloop.swf?clip_id=' + encodeQuotes(url) + '&server=vimeo.com&show_title=1&show_byline=1&show_portrait=0&color=&fullscreen=1&autoplay=1" type="application/x-shockwave-flash" allowfullscreen="true" allowscriptaccess="always" width="640" height="385"></embed></object>';
|
|
|
|
break;
|
|
|
|
case 'theora':
|
|
case 'video':
|
|
code = '<video width="640" height="385" src="' + encodeQuotes(url) + '" controls autoplay><a href="' + encodeQuotes(url) + '" target="_blank">' + encodeQuotes(url) + '</a></video>';
|
|
|
|
break;
|
|
|
|
case 'vorbis':
|
|
case 'audio':
|
|
code = '<audio src="' + encodeQuotes(url) + '" controls autoplay><a href="' + encodeQuotes(url) + '" target="_blank">' + encodeQuotes(url) + '</a></audio>';
|
|
|
|
break;
|
|
|
|
case 'image':
|
|
code = '<a href="' + encodeQuotes(url) + '" target="_blank"><img alt="" src="' + encodeQuotes(url) + '" /></a>';
|
|
|
|
break;
|
|
}
|
|
|
|
return code;
|
|
}
|
|
|
|
// Applies a given integratebox element
|
|
function applyIntegrateBox(url, service, url_list, services_list, comments_e_list, comments_n_list, width_style) {
|
|
// Close the integratebox
|
|
closeIntegrateBox();
|
|
|
|
// Media integration not wanted?
|
|
if(getDB('options', 'integratemedias') == '0')
|
|
return true;
|
|
|
|
// Apply the HTML code
|
|
var dom_code = codeIntegrateBox(service, url);
|
|
|
|
// Any code: apply it!
|
|
if(dom_code) {
|
|
// We show the integratebox
|
|
openIntegrateBox();
|
|
|
|
// We add the code to the DOM
|
|
$('#integratebox .content').prepend('<div class="one-media">' + dom_code + '</div>');
|
|
|
|
// Image waiting icon
|
|
if(service == 'image') {
|
|
var waitItem = $('#integratebox .wait');
|
|
|
|
// Show it while it is loading
|
|
waitItem.show();
|
|
|
|
// Hide it when it is loaded
|
|
$('#integratebox img').load(function() {
|
|
waitItem.hide();
|
|
|
|
// Center the image vertically
|
|
$(this).oneTime(10, function() {
|
|
$(this).css('margin-top', (($('#integratebox .content').height() - $(this).height()) / 2));
|
|
});
|
|
});
|
|
}
|
|
|
|
// Large style?
|
|
var comments_id = genID();
|
|
|
|
if(width_style == 'large') {
|
|
// Make the popup large
|
|
$('#integratebox .popup').addClass('large');
|
|
|
|
// Add the right content
|
|
$('#integratebox .content').after(
|
|
'<div class="comments" data-id="' + encodeQuotes(comments_id) + '">' +
|
|
'<div class="comments-content">' +
|
|
'<div class="one-comment loading"><span class="icon talk-images"></span>' + _e("Loading comments...") + '</div>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
|
|
// Previous and next items?
|
|
var url_array = stringToArray(url_list);
|
|
var services_array = stringToArray(services_list);
|
|
var comments_e_array = stringToArray(comments_e_list);
|
|
var comments_n_array = stringToArray(comments_n_list);
|
|
var index = indexArrayValue(url_array, url);
|
|
|
|
// Any comments?
|
|
if(exists('#integratebox .comments')) {
|
|
if(comments_e_array[index] && comments_n_array[index])
|
|
getCommentsMicroblog(comments_e_array[index], comments_n_array[index], comments_id);
|
|
else
|
|
$('#integratebox .comments .comments-content').html('<div class="one-comment loading"><span class="icon talk-images"></span>' + _e("Comments locked!") + '</div>');
|
|
}
|
|
|
|
// Get the previous values
|
|
var previous_url = url_array[index - 1];
|
|
var previous_services = services_array[index - 1];
|
|
|
|
// Get the next values
|
|
var next_url = url_array[index + 1];
|
|
var next_services = services_array[index + 1];
|
|
|
|
// Enable/disable buttons
|
|
if(previous_url && previous_services)
|
|
$('#integratebox .bottom .finish.previous').removeClass('disabled');
|
|
else
|
|
$('#integratebox .bottom .finish.previous').addClass('disabled');
|
|
|
|
if(next_url && next_services)
|
|
$('#integratebox .bottom .finish.next').removeClass('disabled');
|
|
else
|
|
$('#integratebox .bottom .finish.next').addClass('disabled');
|
|
|
|
// Click events
|
|
$('#integratebox .bottom .finish.previous, #integratebox .bottom .finish.next').click(function() {
|
|
// Not acceptable?
|
|
if($(this).is('.disabled'))
|
|
return false;
|
|
|
|
// Apply the event!
|
|
if($(this).is('.previous'))
|
|
applyIntegrateBox(previous_url, previous_services, url_list, services_list, comments_e_list, comments_n_list, width_style);
|
|
else
|
|
applyIntegrateBox(next_url, next_services, url_list, services_list, comments_e_list, comments_n_list, width_style);
|
|
|
|
return false;
|
|
});
|
|
|
|
if(width_style == 'large')
|
|
$('#integratebox .content a:has(img)').click(function() {
|
|
if(next_url && next_services)
|
|
applyIntegrateBox(next_url, next_services, url_list, services_list, comments_e_list, comments_n_list, width_style);
|
|
|
|
return false;
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Nothing: return true to be able to open the URL in a new tab
|
|
return true;
|
|
}
|
|
|
|
// Checks whether the file ext can use integratebox or not
|
|
function canIntegrateBox(ext) {
|
|
// Can use?
|
|
if(ext && ((ext == 'jpg') || (ext == 'jpeg') || (ext == 'png') || (ext == 'gif') || (ext == 'ogg') || (ext == 'oga') || (ext == 'ogv')))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Filters a string to apply the integratebox links
|
|
function filterIntegrateBox(data) {
|
|
// Encapsulates the string into two <div /> elements
|
|
var xml = $('<div><div>' + data + '</div></div>').contents();
|
|
|
|
// Loop the <a /> elements
|
|
$(xml).find('a').each(function() {
|
|
// Initialize this element
|
|
var href = $(this).attr('href');
|
|
var to, url, service, event;
|
|
|
|
// XMPP ID
|
|
if(href.match(/^xmpp:(.+)/i))
|
|
to = RegExp.$1;
|
|
|
|
// YouTube video box
|
|
else if(href.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) {
|
|
url = RegExp.$8;
|
|
service = 'youtube';
|
|
}
|
|
|
|
// Dailymotion video box
|
|
else if(href.match(/(\w{3,5})(:)(\S+)\.dailymotion\.com\/video\/([\w\-]+)((\#[\w\-]+)|\s|$)/gim)) {
|
|
url = RegExp.$4;
|
|
service = 'dailymotion';
|
|
}
|
|
|
|
// Vimeo video box
|
|
else if(href.match(/((\w{3,5})(:)(\S+)(vimeo|www\.vimeo)\.com\/([\w\-]+))/gim)) {
|
|
url = RegExp.$1;
|
|
service = 'vimeo';
|
|
}
|
|
|
|
// Theora video box
|
|
else if(href.match(/((\w{3,5})(:)(\S+)(\.)(ogv|ogg))/gim)) {
|
|
url = RegExp.$1;
|
|
service = 'theora';
|
|
}
|
|
|
|
// Vorbis audio box
|
|
else if(href.match(/((\w{3,5})(:)(\S+)(\.oga))/gim)) {
|
|
url = RegExp.$1;
|
|
service = 'vorbis';
|
|
}
|
|
|
|
// Image box
|
|
else if(href.match(/((\w{3,5})(:)(\S+)(\.)(jpg|jpeg|png|gif|tif|bmp))/gim)) {
|
|
url = RegExp.$1;
|
|
service = 'image';
|
|
}
|
|
|
|
// Define the good event
|
|
if(to)
|
|
event = 'xmppLink(\'' + encodeOnclick(to) + '\')';
|
|
else if(url && service)
|
|
event = 'applyIntegrateBox(\'' + encodeOnclick(url) + '\', \'' + encodeOnclick(service) + '\')';
|
|
|
|
// Any click event to apply?
|
|
if(event) {
|
|
// Regenerate the link element (for onclick)
|
|
var new_a = '<a';
|
|
var element_a = (this);
|
|
|
|
// Attributes
|
|
$(element_a.attributes).each(function(index) {
|
|
// Read the current attribute
|
|
var current_attr = element_a.attributes[index];
|
|
|
|
// Apply the current attribute
|
|
new_a += ' ' + encodeQuotes(current_attr.name) + '="' + encodeQuotes(current_attr.value) + '"';
|
|
});
|
|
|
|
// Add onclick attribute
|
|
new_a += ' onclick="return ' + event + ';"';
|
|
|
|
// Value
|
|
new_a += '>' + $(this).text().htmlEnc() + '</a>';
|
|
|
|
// Replace it!
|
|
$(this).replaceWith(new_a);
|
|
}
|
|
});
|
|
|
|
// Regenerate the HTML code (include string into a div to be readable)
|
|
var string = $(xml).html();
|
|
|
|
return string;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchIntegratebox() {
|
|
// Click event
|
|
$('#integratebox .bottom .finish.close').click(closeIntegrateBox);
|
|
}
|
|
|
|
// Plugin keyup event
|
|
$(document).keyup(function(e) {
|
|
// Previous item?
|
|
if((exists('#integratebox .bottom .finish.previous:not(.disabled)')) && (e.keyCode == 37)) {
|
|
$('#integratebox .bottom .finish.previous').click();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Next item?
|
|
if((exists('#integratebox .bottom .finish.next:not(.disabled)')) && (e.keyCode == 39)) {
|
|
$('#integratebox .bottom .finish.next').click();
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the PEP JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 26/08/11
|
|
|
|
*/
|
|
|
|
// Stores the PEP items
|
|
function storePEP(xid, type, value1, value2, value3, value4) {
|
|
// Handle the correct values
|
|
if(!value1)
|
|
value1 = '';
|
|
if(!value2)
|
|
value2 = '';
|
|
if(!value3)
|
|
value3 = '';
|
|
if(!value4)
|
|
value4 = '';
|
|
|
|
// If one value
|
|
if(value1 || value2 || value3 || value4) {
|
|
// Define the XML variable
|
|
var xml = '<pep type="' + type + '">';
|
|
|
|
// Generate the correct XML
|
|
if(type == 'tune')
|
|
xml += '<artist>' + value1.htmlEnc() + '</artist><title>' + value2.htmlEnc() + '</title><album>' + value3.htmlEnc() + '</album><uri>' + value4.htmlEnc() + '</uri>';
|
|
else if(type == 'geoloc')
|
|
xml += '<lat>' + value1.htmlEnc() + '</lat><lon>' + value2.htmlEnc() + '</lon><human>' + value3.htmlEnc() + '</human>';
|
|
else
|
|
xml += '<value>' + value1.htmlEnc() + '</value><text>' + value2.htmlEnc() + '</text>';
|
|
|
|
// End the XML node
|
|
xml += '</pep>';
|
|
|
|
// Update the input with the new value
|
|
setDB('pep-' + type, xid, xml);
|
|
}
|
|
|
|
else
|
|
removeDB('pep-' + type, xid);
|
|
|
|
// Display the PEP event
|
|
displayPEP(xid, type);
|
|
}
|
|
|
|
// Displays a PEP item
|
|
function displayPEP(xid, type) {
|
|
// Read the target input for values
|
|
var value = $(XMLFromString(getDB('pep-' + type, xid)));
|
|
var dText;
|
|
var aLink = ''
|
|
|
|
// If the PEP element exists
|
|
if(type) {
|
|
// Get the user hash
|
|
var hash = hex_md5(xid);
|
|
|
|
// Initialize
|
|
var fText, fValue;
|
|
var dText = '';
|
|
|
|
// Parse the XML for mood and activity
|
|
if((type == 'mood') || (type == 'activity')) {
|
|
if(value) {
|
|
var pepValue = value.find('value').text();
|
|
var pepText = value.find('text').text();
|
|
|
|
// No value?
|
|
if(!pepValue)
|
|
pepValue = 'none';
|
|
|
|
// Apply the good values
|
|
if(type == 'mood')
|
|
fValue = moodIcon(pepValue);
|
|
else if(type == 'activity')
|
|
fValue = activityIcon(pepValue);
|
|
if(!pepText)
|
|
fText = _e("unknown");
|
|
else
|
|
fText = pepText;
|
|
}
|
|
|
|
else {
|
|
if(type == 'mood')
|
|
fValue = moodIcon('undefined');
|
|
else if(type == 'activity')
|
|
fValue = activityIcon('exercising');
|
|
|
|
fText = _e("unknown");
|
|
}
|
|
|
|
dText = fText;
|
|
fText = fText.htmlEnc();
|
|
}
|
|
|
|
else if(type == 'tune') {
|
|
fValue = 'tune-note';
|
|
|
|
if(value) {
|
|
// Parse the tune XML
|
|
var tArtist = value.find('artist').text();
|
|
var tTitle = value.find('title').text();
|
|
var tAlbum = value.find('album').text();
|
|
var tURI = value.find('uri').text();
|
|
var fArtist, fTitle, fAlbum, fURI;
|
|
|
|
// Apply the good values
|
|
if(!tArtist && !tAlbum && !tTitle) {
|
|
fText = _e("unknown");
|
|
dText = fText;
|
|
}
|
|
|
|
else {
|
|
// URI element
|
|
if(!tURI)
|
|
fURI = 'http://grooveshark.com/search?q=' + encodeURIComponent(tArtist + ' ' + tTitle + ' ' + tAlbum);
|
|
else
|
|
fURI = tURI;
|
|
|
|
// Artist element
|
|
if(!tArtist)
|
|
fArtist = _e("unknown");
|
|
else
|
|
fArtist = tArtist;
|
|
|
|
// Title element
|
|
if(!tTitle)
|
|
fTitle = _e("unknown");
|
|
else
|
|
fTitle = tTitle;
|
|
|
|
// Album element
|
|
if(!tAlbum)
|
|
fAlbum = _e("unknown");
|
|
else
|
|
fAlbum = tAlbum;
|
|
|
|
// Generate the link to the title
|
|
aLink = ' href="' + fURI + '" target="_blank"';
|
|
|
|
// Generate the text to be displayed
|
|
dText = fArtist + ' - ' + fTitle + ' (' + fAlbum + ')';
|
|
fText = '<a' + aLink + '>' + dText + '</a>';
|
|
}
|
|
}
|
|
|
|
else {
|
|
fText = _e("unknown");
|
|
dText = fText;
|
|
}
|
|
}
|
|
|
|
else if(type == 'geoloc') {
|
|
fValue = 'location-world';
|
|
|
|
if(value) {
|
|
// Parse the geoloc XML
|
|
var tLat = value.find('lat').text();
|
|
var tLon = value.find('lon').text();
|
|
var tHuman = value.find('human').text();
|
|
var tReal = tHuman;
|
|
|
|
// No human location?
|
|
if(!tHuman)
|
|
tHuman = _e("See his/her position on the globe");
|
|
|
|
// Generate the text to be displayed
|
|
if(tLat && tLon) {
|
|
aLink = ' href="http://maps.google.com/?q=' + encodeQuotes(tLat) + ',' + encodeQuotes(tLon) + '" target="_blank"';
|
|
fText = '<a' + aLink + '>' + tHuman.htmlEnc() + '</a>';
|
|
|
|
if(tReal)
|
|
dText = tReal;
|
|
else
|
|
dText = tLat + '; ' + tLon;
|
|
}
|
|
|
|
else {
|
|
fText = _e("unknown");
|
|
dText = fText;
|
|
}
|
|
}
|
|
|
|
else {
|
|
fText = _e("unknown");
|
|
dText = fText;
|
|
}
|
|
}
|
|
|
|
// Apply the text to the buddy infos
|
|
var this_buddy = '#buddy-list .buddy[data-xid="' + escape(xid) + '"]';
|
|
|
|
if(exists(this_buddy))
|
|
$(this_buddy + ' .bi-' + type).replaceWith('<p class="bi-' + type + ' talk-images ' + fValue + '" title="' + encodeQuotes(dText) + '">' + fText + '</p>');
|
|
|
|
// Apply the text to the buddy chat
|
|
if(exists('#' + hash)) {
|
|
// Selector
|
|
var bc_pep = $('#' + hash + ' .bc-pep');
|
|
|
|
// We remove the old PEP item
|
|
bc_pep.find('a.bi-' + type).remove();
|
|
|
|
// If the new PEP item is not null, create a new one
|
|
if(fText != _e("unknown"))
|
|
bc_pep.prepend(
|
|
'<a' + aLink + ' class="bi-' + type + ' talk-images ' + fValue + '" title="' + encodeQuotes(dText) + '"></a>'
|
|
);
|
|
|
|
// Process the new status position
|
|
adaptChatPresence(hash);
|
|
}
|
|
|
|
// If this is the PEP values of the logged in user
|
|
if(xid == getXID()) {
|
|
// Change the icon/value of the target element
|
|
if((type == 'mood') || (type == 'activity')) {
|
|
// Change the input value
|
|
var dVal = '';
|
|
var dAttr = pepValue;
|
|
|
|
// Must apply default values?
|
|
if(pepValue == 'none') {
|
|
if(type == 'mood')
|
|
dAttr = 'happy';
|
|
else
|
|
dAttr = 'exercising';
|
|
}
|
|
|
|
// No text?
|
|
if(dText != _e("unknown"))
|
|
dVal = dText;
|
|
|
|
// Store this user event in our database
|
|
setDB(type + '-value', 1, dAttr);
|
|
setDB(type + '-text', 1, dVal);
|
|
|
|
// Apply this PEP event
|
|
$('#my-infos .f-' + type + ' a.picker').attr('data-value', dAttr);
|
|
$('#my-infos .f-' + type + ' input').val(dVal);
|
|
$('#my-infos .f-' + type + ' input').placeholder();
|
|
}
|
|
|
|
else if((type == 'tune') || (type == 'geoloc')) {
|
|
// Reset the values
|
|
$('#my-infos .f-others a.' + type).remove();
|
|
|
|
// Not empty?
|
|
if(dText != _e("unknown")) {
|
|
// Specific stuffs
|
|
var href, title, icon_class;
|
|
|
|
if(type == 'tune') {
|
|
href = fURI;
|
|
title = dText;
|
|
icon_class = 'tune-note';
|
|
}
|
|
|
|
else {
|
|
href = 'http://maps.google.com/?q=' + encodeQuotes(tLat) + ',' + encodeQuotes(tLon);
|
|
title = _e("Where are you?") + ' (' + dText + ')';
|
|
icon_class = 'location-world';
|
|
}
|
|
|
|
// Must create the container?
|
|
if(!exists('#my-infos .f-others'))
|
|
$('#my-infos .content').append('<div class="element f-others"></div>');
|
|
|
|
// Create the element
|
|
$('#my-infos .f-others').prepend(
|
|
'<a class="icon ' + type + '" href="' + encodeQuotes(href) + '" target="_blank" title="' + encodeQuotes(title) + '">' +
|
|
'<span class="talk-images ' + icon_class + '"></span>' +
|
|
'</a>'
|
|
);
|
|
}
|
|
|
|
// Empty?
|
|
else if(!exists('#my-infos .f-others a.icon'))
|
|
$('#my-infos .f-others').remove();
|
|
|
|
// Process the buddy-list height again
|
|
adaptRoster();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Changes the mood icon
|
|
function moodIcon(value) {
|
|
// The main var
|
|
var icon;
|
|
|
|
// Switch the values
|
|
switch(value) {
|
|
case 'angry':
|
|
case 'cranky':
|
|
case 'hot':
|
|
case 'invincible':
|
|
case 'mean':
|
|
case 'restless':
|
|
case 'serious':
|
|
case 'strong':
|
|
icon = 'mood-one';
|
|
break;
|
|
|
|
case 'contemplative':
|
|
case 'happy':
|
|
case 'playful':
|
|
icon = 'mood-two';
|
|
break;
|
|
|
|
case 'aroused':
|
|
case 'envious':
|
|
case 'excited':
|
|
case 'interested':
|
|
case 'lucky':
|
|
case 'proud':
|
|
case 'relieved':
|
|
case 'satisfied':
|
|
case 'shy':
|
|
icon = 'mood-three';
|
|
break;
|
|
|
|
case 'calm':
|
|
case 'cautious':
|
|
case 'contented':
|
|
case 'creative':
|
|
case 'humbled':
|
|
case 'lonely':
|
|
case 'undefined':
|
|
case 'none':
|
|
icon = 'mood-four';
|
|
break;
|
|
|
|
case 'afraid':
|
|
case 'amazed':
|
|
case 'confused':
|
|
case 'dismayed':
|
|
case 'hungry':
|
|
case 'in_awe':
|
|
case 'indignant':
|
|
case 'jealous':
|
|
case 'lost':
|
|
case 'offended':
|
|
case 'outraged':
|
|
case 'shocked':
|
|
case 'surprised':
|
|
case 'embarrassed':
|
|
case 'impressed':
|
|
icon = 'mood-five';
|
|
break;
|
|
|
|
case 'crazy':
|
|
case 'distracted':
|
|
case 'neutral':
|
|
case 'relaxed':
|
|
case 'thirsty':
|
|
icon = 'mood-six';
|
|
break;
|
|
|
|
case 'amorous':
|
|
case 'curious':
|
|
case 'in_love':
|
|
case 'nervous':
|
|
case 'sarcastic':
|
|
icon = 'mood-eight';
|
|
break;
|
|
|
|
case 'brave':
|
|
case 'confident':
|
|
case 'hopeful':
|
|
case 'grateful':
|
|
case 'spontaneous':
|
|
case 'thankful':
|
|
icon = 'mood-nine';
|
|
break;
|
|
|
|
default:
|
|
icon = 'mood-seven';
|
|
break;
|
|
}
|
|
|
|
// Return the good icon name
|
|
return icon;
|
|
}
|
|
|
|
// Changes the activity icon
|
|
function activityIcon(value) {
|
|
// The main var
|
|
var icon;
|
|
|
|
// Switch the values
|
|
switch(value) {
|
|
case 'doing_chores':
|
|
icon = 'activity-doing_chores';
|
|
break;
|
|
|
|
case 'drinking':
|
|
icon = 'activity-drinking';
|
|
break;
|
|
|
|
case 'eating':
|
|
icon = 'activity-eating';
|
|
break;
|
|
|
|
case 'grooming':
|
|
icon = 'activity-grooming';
|
|
break;
|
|
|
|
case 'having_appointment':
|
|
icon = 'activity-having_appointment';
|
|
break;
|
|
|
|
case 'inactive':
|
|
icon = 'activity-inactive';
|
|
break;
|
|
|
|
case 'relaxing':
|
|
icon = 'activity-relaxing';
|
|
break;
|
|
|
|
case 'talking':
|
|
icon = 'activity-talking';
|
|
break;
|
|
|
|
case 'traveling':
|
|
icon = 'activity-traveling';
|
|
break;
|
|
|
|
case 'working':
|
|
icon = 'activity-working';
|
|
break;
|
|
default:
|
|
icon = 'activity-exercising';
|
|
break;
|
|
}
|
|
|
|
// Return the good icon name
|
|
return icon;
|
|
}
|
|
|
|
// Sends the user's mood
|
|
function sendMood(value, text) {
|
|
/* REF: http://xmpp.org/extensions/xep-0107.html */
|
|
|
|
// We propagate the mood on the xmpp network
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// We create the XML document
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_MOOD, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB}));
|
|
var mood = item.appendChild(iq.buildNode('mood', {'xmlns': NS_MOOD}));
|
|
|
|
if(value != 'none') {
|
|
mood.appendChild(iq.buildNode(value, {'xmlns': NS_MOOD}));
|
|
mood.appendChild(iq.buildNode('text', {'xmlns': NS_MOOD}, text));
|
|
}
|
|
|
|
// And finally we send the mood that is set
|
|
con.send(iq);
|
|
|
|
logThis('New mood sent: ' + value + ' (' + text + ')', 3);
|
|
}
|
|
|
|
// Sends the user's activity
|
|
function sendActivity(main, sub, text) {
|
|
// We propagate the mood on the xmpp network
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// We create the XML document
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_ACTIVITY, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB}));
|
|
var activity = item.appendChild(iq.buildNode('activity', {'xmlns': NS_ACTIVITY}));
|
|
|
|
if(main != 'none') {
|
|
var mainType = activity.appendChild(iq.buildNode(main, {'xmlns': NS_ACTIVITY}));
|
|
|
|
// Child nodes
|
|
if(sub)
|
|
mainType.appendChild(iq.buildNode(sub, {'xmlns': NS_ACTIVITY}));
|
|
if(text)
|
|
activity.appendChild(iq.buildNode('text', {'xmlns': NS_ACTIVITY}, text));
|
|
}
|
|
|
|
// And finally we send the mood that is set
|
|
con.send(iq);
|
|
|
|
logThis('New activity sent: ' + main + ' (' + text + ')', 3);
|
|
}
|
|
|
|
// Sends the user's geographic position
|
|
function sendPosition(vLat, vLon, vAlt, vCountry, vCountrycode, vRegion, vPostalcode, vLocality, vStreet, vBuilding, vText, vURI) {
|
|
/* REF: http://xmpp.org/extensions/xep-0080.html */
|
|
|
|
// We propagate the position on pubsub
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
// We create the XML document
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_GEOLOC, 'xmlns': NS_PUBSUB}));
|
|
var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB}));
|
|
var geoloc = item.appendChild(iq.buildNode('geoloc', {'xmlns': NS_GEOLOC}));
|
|
|
|
// Create two position arrays
|
|
var pos_names = ['lat', 'lon', 'alt', 'country', 'countrycode', 'region', 'postalcode', 'locality', 'street', 'building', 'text', 'uri', 'timestamp'];
|
|
var pos_values = [ vLat, vLon, vAlt, vCountry, vCountrycode, vRegion, vPostalcode, vLocality, vStreet, vBuilding, vText, vURI, getXMPPTime('utc')];
|
|
|
|
for(var i = 0; i < pos_names.length; i++) {
|
|
if(pos_names[i] && pos_values[i])
|
|
geoloc.appendChild(iq.buildNode(pos_names[i], {'xmlns': NS_GEOLOC}, pos_values[i]));
|
|
}
|
|
|
|
// And finally we send the XML
|
|
con.send(iq);
|
|
|
|
// For logger
|
|
if(vLat && vLon)
|
|
logThis('Geolocated.', 3);
|
|
else
|
|
logThis('Not geolocated.', 2);
|
|
}
|
|
|
|
// Parses the user's geographic position
|
|
function parsePosition(data) {
|
|
var result = $(data).find('result:first');
|
|
|
|
// Get latitude and longitude
|
|
var lat = result.find('geometry:first location:first lat').text();
|
|
var lng = result.find('geometry:first location:first lng').text();
|
|
|
|
var array = [
|
|
lat,
|
|
lng,
|
|
result.find('address_component:has(type:contains("country")):first long_name').text(),
|
|
result.find('address_component:has(type:contains("country")):first short_name').text(),
|
|
result.find('address_component:has(type:contains("administrative_area_level_1")):first long_name').text(),
|
|
result.find('address_component:has(type:contains("postal_code")):first long_name').text(),
|
|
result.find('address_component:has(type:contains("locality")):first long_name').text(),
|
|
result.find('address_component:has(type:contains("route")):first long_name').text(),
|
|
result.find('address_component:has(type:contains("street_number")):first long_name').text(),
|
|
result.find('formatted_address:first').text(),
|
|
'http://maps.google.com/?q=' + encodeQuotes(lat) + ',' + encodeQuotes(lng)
|
|
];
|
|
|
|
return array;
|
|
}
|
|
|
|
// Converts a position into an human-readable one
|
|
function humanPosition(tLocality, tRegion, tCountry) {
|
|
var tHuman = '';
|
|
|
|
// Any locality?
|
|
if(tLocality) {
|
|
tHuman += tLocality;
|
|
|
|
if(tRegion)
|
|
tHuman += ', ' + tRegion;
|
|
if(tCountry)
|
|
tHuman += ', ' + tCountry;
|
|
}
|
|
|
|
// Any region?
|
|
else if(tRegion) {
|
|
tHuman += tRegion;
|
|
|
|
if(tCountry)
|
|
tHuman += ', ' + tCountry;
|
|
}
|
|
|
|
// Any country?
|
|
else if(tCountry)
|
|
tHuman += tCountry;
|
|
|
|
return tHuman;
|
|
}
|
|
|
|
// Gets the user's geographic position
|
|
function getPosition(position) {
|
|
// Convert integers to strings
|
|
var vLat = '' + position.coords.latitude;
|
|
var vLon = '' + position.coords.longitude;
|
|
var vAlt = '' + position.coords.altitude;
|
|
|
|
// Get full position (from Google Maps API)
|
|
$.get('./php/geolocation.php', {latitude: vLat, longitude: vLon, language: XML_LANG}, function(data) {
|
|
// Parse data!
|
|
var results = parsePosition(data);
|
|
|
|
// Handled!
|
|
sendPosition(
|
|
isNumber(vLat) ? vLat : null,
|
|
isNumber(vLon) ? vLon : null,
|
|
isNumber(vAlt) ? vAlt : null,
|
|
results[2],
|
|
results[3],
|
|
results[4],
|
|
results[5],
|
|
results[6],
|
|
results[7],
|
|
results[8],
|
|
results[9],
|
|
results[10]
|
|
);
|
|
|
|
// Store data
|
|
setDB('geolocation', 'now', xmlToString(data));
|
|
|
|
logThis('Position details got from Google Maps API.');
|
|
});
|
|
|
|
logThis('Position got: latitude > ' + vLat + ' / longitude > ' + vLon + ' / altitude > ' + vAlt);
|
|
}
|
|
|
|
// Geolocates the user
|
|
function geolocate() {
|
|
// Don't fire it until options & features are not retrieved!
|
|
if(!getDB('options', 'geolocation') || (getDB('options', 'geolocation') == '0') || !enabledPEP())
|
|
return;
|
|
|
|
// We publish the user location if allowed
|
|
if(navigator.geolocation) {
|
|
// Wait a bit... (to fix a bug)
|
|
$('#my-infos').stopTime().oneTime('1s', function() {
|
|
navigator.geolocation.getCurrentPosition(getPosition);
|
|
});
|
|
|
|
logThis('Geolocating...', 3);
|
|
}
|
|
|
|
// Any error?
|
|
else
|
|
logThis('Not geolocated: browser does not support it.', 1);
|
|
}
|
|
|
|
// Gets the user's microblog to check it exists
|
|
function getInitGeoloc() {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var ps_items = pubsub.appendChild(iq.buildNode('items', {'node': NS_GEOLOC, 'xmlns': NS_PUBSUB}));
|
|
|
|
ps_items.setAttribute('max_items', '0');
|
|
|
|
con.send(iq, handleInitGeoloc);
|
|
}
|
|
|
|
// Handles the user's microblog to create it in case of error
|
|
function handleInitGeoloc(iq) {
|
|
// Any error?
|
|
if((iq.getType() == 'error') && $(iq.getNode()).find('item-not-found').size()) {
|
|
// The node may not exist, create it!
|
|
setupMicroblog('', NS_GEOLOC, '1', '1', '', '', true);
|
|
|
|
logThis('Error while getting geoloc, trying to reconfigure the PubSub node!', 2);
|
|
}
|
|
}
|
|
|
|
// Displays all the supported PEP events for a given XID
|
|
function displayAllPEP(xid) {
|
|
displayPEP(xid, 'mood');
|
|
displayPEP(xid, 'activity');
|
|
displayPEP(xid, 'tune');
|
|
displayPEP(xid, 'geoloc');
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchPEP() {
|
|
// Apply empty values to the PEP database
|
|
setDB('mood-value', 1, '');
|
|
setDB('mood-text', 1, '');
|
|
setDB('activity-value', 1, '');
|
|
setDB('activity-text', 1, '');
|
|
|
|
// Click event for user mood
|
|
$('#my-infos .f-mood a.picker').click(function() {
|
|
// Initialize some vars
|
|
var path = '#my-infos .f-mood div.bubble';
|
|
var mood_id = ['crazy', 'excited', 'playful', 'happy', 'shocked', 'hot', 'sad', 'amorous', 'confident'];
|
|
var mood_lang = [_e("Crazy"), _e("Excited"), _e("Playful"), _e("Happy"), _e("Shocked"), _e("Hot"), _e("Sad"), _e("Amorous"), _e("Confident")];
|
|
var mood_val = $('#my-infos .f-mood a.picker').attr('data-value');
|
|
|
|
// Yet displayed?
|
|
var can_append = true;
|
|
|
|
if(exists(path))
|
|
can_append = false;
|
|
|
|
// Add this bubble!
|
|
showBubble(path);
|
|
|
|
if(!can_append)
|
|
return false;
|
|
|
|
// Generate the HTML code
|
|
var html = '<div class="bubble removable">';
|
|
|
|
for(i in mood_id) {
|
|
// Yet in use: no need to display it!
|
|
if(mood_id[i] == mood_val)
|
|
continue;
|
|
|
|
html += '<a href="#" class="talk-images" data-value="' + mood_id[i] + '" title="' + mood_lang[i] + '"></a>';
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
// Append the HTML code
|
|
$('#my-infos .f-mood').append(html);
|
|
|
|
// Click event
|
|
$(path + ' a').click(function() {
|
|
// Update the mood marker
|
|
$('#my-infos .f-mood a.picker').attr('data-value', $(this).attr('data-value'));
|
|
|
|
// Close the bubble
|
|
closeBubbles();
|
|
|
|
// Focus on the status input
|
|
$(document).oneTime(10, function() {
|
|
$('#mood-text').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
// Click event for user activity
|
|
$('#my-infos .f-activity a.picker').click(function() {
|
|
// Initialize some vars
|
|
var path = '#my-infos .f-activity div.bubble';
|
|
var activity_id = ['doing_chores', 'drinking', 'eating', 'exercising', 'grooming', 'having_appointment', 'inactive', 'relaxing', 'talking', 'traveling', 'working'];
|
|
var activity_lang = [_e("Chores"), _e("Drinking"), _e("Eating"), _e("Exercising"), _e("Grooming"), _e("Appointment"), _e("Inactive"), _e("Relaxing"), _e("Talking"), _e("Traveling"), _e("Working")];
|
|
var activity_val = $('#my-infos .f-activity a.picker').attr('data-value');
|
|
|
|
// Yet displayed?
|
|
var can_append = true;
|
|
|
|
if(exists(path))
|
|
can_append = false;
|
|
|
|
// Add this bubble!
|
|
showBubble(path);
|
|
|
|
if(!can_append)
|
|
return false;
|
|
|
|
// Generate the HTML code
|
|
var html = '<div class="bubble removable">';
|
|
|
|
for(i in activity_id) {
|
|
// Yet in use: no need to display it!
|
|
if(activity_id[i] == activity_val)
|
|
continue;
|
|
|
|
html += '<a href="#" class="talk-images" data-value="' + activity_id[i] + '" title="' + activity_lang[i] + '"></a>';
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
// Append the HTML code
|
|
$('#my-infos .f-activity').append(html);
|
|
|
|
// Click event
|
|
$(path + ' a').click(function() {
|
|
// Update the activity marker
|
|
$('#my-infos .f-activity a.picker').attr('data-value', $(this).attr('data-value'));
|
|
|
|
// Close the bubble
|
|
closeBubbles();
|
|
|
|
// Focus on the status input
|
|
$(document).oneTime(10, function() {
|
|
$('#activity-text').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
// Submit events for PEP inputs
|
|
$('#mood-text, #activity-text').placeholder()
|
|
|
|
.keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
$(this).blur();
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Input blur handler
|
|
$('#mood-text').blur(function() {
|
|
// Read the parameters
|
|
var value = $('#my-infos .f-mood a.picker').attr('data-value');
|
|
var text = $(this).val();
|
|
|
|
// Must send the mood?
|
|
if((value != getDB('mood-value', 1)) || (text != getDB('mood-text', 1))) {
|
|
// Update the local stored values
|
|
setDB('mood-value', 1, value);
|
|
setDB('mood-text', 1, text);
|
|
|
|
// Send it!
|
|
sendMood(value, text);
|
|
}
|
|
})
|
|
|
|
// Input focus handler
|
|
.focus(function() {
|
|
closeBubbles();
|
|
});
|
|
|
|
// Input blur handler
|
|
$('#activity-text').blur(function() {
|
|
// Read the parameters
|
|
var value = $('#my-infos .f-activity a.picker').attr('data-value');
|
|
var text = $(this).val();
|
|
|
|
// Must send the activity?
|
|
if((value != getDB('activity-value', 1)) || (text != getDB('activity-text', 1))) {
|
|
// Update the local stored values
|
|
setDB('activity-value', 1, value);
|
|
setDB('activity-text', 1, text);
|
|
|
|
// Send it!
|
|
sendActivity(value, '', text);
|
|
}
|
|
})
|
|
|
|
// Input focus handler
|
|
.focus(function() {
|
|
closeBubbles();
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the presence JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 21/06/12
|
|
|
|
*/
|
|
|
|
// Sends the user first presence
|
|
var FIRST_PRESENCE_SENT = false;
|
|
|
|
function firstPresence(checksum) {
|
|
logThis('First presence sent.', 3);
|
|
|
|
// Jappix is now ready: change the title
|
|
pageTitle('talk');
|
|
|
|
// Anonymous check
|
|
var is_anonymous = isAnonymous();
|
|
|
|
// Update our marker
|
|
FIRST_PRESENCE_SENT = true;
|
|
|
|
// Try to use the last status message
|
|
var status = getDB('options', 'presence-status');
|
|
|
|
if(!status)
|
|
status = '';
|
|
|
|
// We tell the world that we are online
|
|
if(!is_anonymous)
|
|
sendPresence('', '', '', status, checksum);
|
|
|
|
// Any status to apply?
|
|
if(status)
|
|
$('#presence-status').val(status);
|
|
|
|
// Enable the presence picker
|
|
$('#presence-status').removeAttr('disabled');
|
|
$('#my-infos .f-presence a.picker').removeClass('disabled');
|
|
|
|
// We set the last activity stamp
|
|
PRESENCE_LAST_ACTIVITY = getTimeStamp();
|
|
|
|
// We store our presence
|
|
setDB('presence-show', 1, 'available');
|
|
|
|
// Not anonymous
|
|
if(!is_anonymous) {
|
|
// We get the stored bookmarks (because of the photo hash and some other stuffs, we must get it later)
|
|
getStorage(NS_BOOKMARKS);
|
|
|
|
// We open a new chat if a XMPP link was submitted
|
|
if((parent.location.hash != '#OK') && LINK_VARS['x']) {
|
|
// A link is submitted in the URL
|
|
xmppLink(LINK_VARS['x']);
|
|
|
|
// Set a OK status
|
|
parent.location.hash = 'OK';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handles incoming presence packets
|
|
function handlePresence(presence) {
|
|
// We define everything needed here
|
|
var from = fullXID(getStanzaFrom(presence));
|
|
var hash = hex_md5(from);
|
|
var node = presence.getNode();
|
|
var xid = bareXID(from);
|
|
var xidHash = hex_md5(xid);
|
|
var resource = thisResource(from);
|
|
|
|
// We get the type content
|
|
var type = presence.getType();
|
|
if(!type)
|
|
type = '';
|
|
|
|
// We get the priority content
|
|
var priority = presence.getPriority() + '';
|
|
if(!priority || (type == 'error'))
|
|
priority = '0';
|
|
|
|
// We get the show content
|
|
var show = presence.getShow();
|
|
if(!show || (type == 'error'))
|
|
show = '';
|
|
|
|
// We get the status content
|
|
var status = presence.getStatus();
|
|
if(!status || (type == 'error'))
|
|
status = '';
|
|
|
|
// We get the photo content
|
|
var photo = $(node).find('x[xmlns="' + NS_VCARD_P + '"]:first photo');
|
|
var checksum = photo.text();
|
|
var hasPhoto = photo.size();
|
|
|
|
if(hasPhoto && (type != 'error'))
|
|
hasPhoto = 'true';
|
|
else
|
|
hasPhoto = 'false';
|
|
|
|
// We get the CAPS content
|
|
var caps = $(node).find('c[xmlns="' + NS_CAPS + '"]:first').attr('ver');
|
|
if(!caps || (type == 'error'))
|
|
caps = '';
|
|
|
|
// This presence comes from another resource of my account with a difference avatar checksum
|
|
if((xid == getXID()) && (hasPhoto == 'true') && (checksum != getDB('checksum', 1)))
|
|
getAvatar(getXID(), 'force', 'true', 'forget');
|
|
|
|
// This presence comes from a groupchat
|
|
if(isPrivate(xid)) {
|
|
var x_muc = $(node).find('x[xmlns="' + NS_MUC_USER + '"]:first');
|
|
var item = x_muc.find('item');
|
|
var affiliation = item.attr('affiliation');
|
|
var role = item.attr('role');
|
|
var reason = item.find('reason').text();
|
|
var iXID = item.attr('jid');
|
|
var iNick = item.attr('nick');
|
|
var nick = resource;
|
|
var messageTime = getCompleteTime();
|
|
var notInitial = true;
|
|
var resources_obj;
|
|
|
|
// Read the status code
|
|
var status_code = new Array();
|
|
|
|
x_muc.find('status').each(function() {
|
|
status_code.push(parseInt($(this).attr('code')));
|
|
});
|
|
|
|
// If this is an initial presence (when user join the room)
|
|
if(exists('#' + xidHash + '[data-initial="true"]'))
|
|
notInitial = false;
|
|
|
|
// If one user is quitting
|
|
if(type && (type == 'unavailable')) {
|
|
displayMucPresence(from, xidHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, messageTime, nick, notInitial);
|
|
|
|
removeDB('presence-stanza', from);
|
|
resources_obj = removeResourcePresence(xid, resource);
|
|
}
|
|
|
|
// If one user is joining
|
|
else {
|
|
// Fixes M-Link first presence bug (missing ID!)
|
|
if((nick == getMUCNick(xidHash)) && (presence.getID() == null) && !exists('#page-engine #' + xidHash + ' .list .' + hash)) {
|
|
handleMUC(presence);
|
|
|
|
logThis('Passed M-Link MUC first presence handling.', 2);
|
|
}
|
|
|
|
else {
|
|
displayMucPresence(from, xidHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, messageTime, nick, notInitial);
|
|
|
|
var xml = '<presence from="' + encodeQuotes(from) + '"><priority>' + priority.htmlEnc() + '</priority><show>' + show.htmlEnc() + '</show><type>' + type.htmlEnc() + '</type><status>' + status.htmlEnc() + '</status><avatar>' + hasPhoto.htmlEnc() + '</avatar><checksum>' + checksum.htmlEnc() + '</checksum><caps>' + caps.htmlEnc() + '</caps></presence>';
|
|
|
|
setDB('presence-stanza', from, xml);
|
|
resources_obj = addResourcePresence(xid, resource);
|
|
}
|
|
}
|
|
|
|
// Manage the presence
|
|
processPriority(from, resource, resources_obj);
|
|
presenceFunnel(from, hash);
|
|
}
|
|
|
|
// This presence comes from an user or a gateway
|
|
else {
|
|
// Subscribed/Unsubscribed stanzas
|
|
if((type == 'subscribed') || (type == 'unsubscribed'))
|
|
return;
|
|
// Subscribe stanza
|
|
else if(type == 'subscribe') {
|
|
// This is a buddy we can safely authorize, because we added him to our roster
|
|
if(exists('#buddy-list .buddy[data-xid="' + escape(xid) + '"]'))
|
|
acceptSubscribe(xid);
|
|
|
|
// We do not know this entity, we'd be better ask the user
|
|
else {
|
|
// Get the nickname
|
|
var nickname = $(node).find('nick[xmlns="' + NS_NICK + '"]:first').text();
|
|
|
|
// New notification
|
|
newNotification('subscribe', xid, [xid, nickname], status);
|
|
}
|
|
}
|
|
|
|
// Unsubscribe stanza
|
|
else if(type == 'unsubscribe')
|
|
sendRoster(xid, 'remove');
|
|
|
|
// Other stanzas
|
|
else {
|
|
var resources_obj;
|
|
|
|
// Unavailable/error presence
|
|
if(type == 'unavailable') {
|
|
removeDB('presence-stanza', from);
|
|
resources_obj = removeResourcePresence(xid, resource);
|
|
}
|
|
|
|
// Other presence (available, subscribe...)
|
|
else {
|
|
var xml = '<presence from="' + encodeQuotes(from) + '"><priority>' + priority.htmlEnc() + '</priority><show>' + show.htmlEnc() + '</show><type>' + type.htmlEnc() + '</type><status>' + status.htmlEnc() + '</status><avatar>' + hasPhoto.htmlEnc() + '</avatar><checksum>' + checksum.htmlEnc() + '</checksum><caps>' + caps.htmlEnc() + '</caps></presence>';
|
|
|
|
setDB('presence-stanza', from, xml);
|
|
resources_obj = addResourcePresence(xid, resource);
|
|
}
|
|
|
|
// We manage the presence
|
|
processPriority(xid, resource, resources_obj);
|
|
presenceFunnel(xid, xidHash);
|
|
|
|
// We display the presence in the current chat
|
|
if(exists('#' + xidHash)) {
|
|
var dStatus = filterStatus(xid, status, false);
|
|
|
|
if(dStatus)
|
|
dStatus = ' (' + dStatus + ')';
|
|
|
|
// Generate the presence-in-chat code
|
|
var dName = getBuddyName(from).htmlEnc();
|
|
var dBody = dName + ' (' + from + ') ' + _e("is now") + ' ' + humanShow(show, type) + dStatus;
|
|
|
|
// Check whether it has been previously displayed
|
|
var can_display = true;
|
|
|
|
if($('#' + xidHash + ' .one-line.system-message:last').html() == dBody)
|
|
can_display = false;
|
|
|
|
if(can_display)
|
|
displayMessage('chat', xid, xidHash, dName, dBody, getCompleteTime(), getTimeStamp(), 'system-message', false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// For logger
|
|
if(!show) {
|
|
if(!type)
|
|
show = 'available';
|
|
else
|
|
show = 'unavailable';
|
|
}
|
|
|
|
logThis('Presence received: ' + show + ', from ' + from);
|
|
}
|
|
|
|
// Displays a MUC presence
|
|
function displayMucPresence(from, roomHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, messageTime, nick, initial) {
|
|
// Generate the values
|
|
var thisUser = '#page-engine #' + roomHash + ' .list .' + hash;
|
|
var thisPrivate = $('#' + hash + ' .message-area');
|
|
var nick_html = nick.htmlEnc();
|
|
var real_xid = '';
|
|
var write = nick_html + ' ';
|
|
var notify = false;
|
|
|
|
// Reset data?
|
|
if(!role)
|
|
role = 'participant';
|
|
if(!affiliation)
|
|
affiliation = 'none';
|
|
|
|
// Must update the role?
|
|
if(exists(thisUser) && (($(thisUser).attr('data-role') != role) || ($(thisUser).attr('data-affiliation') != affiliation)))
|
|
$(thisUser).remove();
|
|
|
|
// Any XID submitted?
|
|
if(iXID) {
|
|
real_xid = ' data-realxid="' + iXID + '"';
|
|
iXID = bareXID(iXID);
|
|
write += ' (<a onclick="return checkChatCreate(\'' + encodeOnclick(iXID) + '\', \'chat\');" href="xmpp:' + encodeOnclick(iXID) + '">' + iXID + '</a>) ';
|
|
}
|
|
|
|
// User does not exists yet
|
|
if(!exists(thisUser) && (!type || (type == 'available'))) {
|
|
var myself = '';
|
|
|
|
// Is it me?
|
|
if(nick == getMUCNick(roomHash)) {
|
|
// Enable the room
|
|
$('#' + roomHash + ' .message-area').removeAttr('disabled');
|
|
|
|
// Marker
|
|
myself = ' myself';
|
|
}
|
|
|
|
// Set the user in the MUC list
|
|
$('#' + roomHash + ' .list .' + role + ' .title').after(
|
|
'<div class="user ' + hash + myself + '" data-xid="' + encodeQuotes(from) + '" data-nick="' + escape(nick) + '"' + real_xid + ' data-role="' + encodeQuotes(role) + '" data-affiliation="' + encodeQuotes(affiliation) + '">' +
|
|
'<div class="name talk-images available">' + nick_html + '</div>' +
|
|
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Click event
|
|
if(nick != getMUCNick(roomHash))
|
|
$(thisUser).on('click', function() {
|
|
checkChatCreate(from, 'private');
|
|
});
|
|
|
|
// We tell the user that someone entered the room
|
|
if(!initial) {
|
|
notify = true;
|
|
write += _e("joined the chat room");
|
|
|
|
// Any status?
|
|
if(status)
|
|
write += ' (' + filterThisMessage(status, nick_html, true) + ')';
|
|
else
|
|
write += ' (' + _e("no status") + ')';
|
|
}
|
|
|
|
// Enable the private chat input
|
|
thisPrivate.removeAttr('disabled');
|
|
}
|
|
|
|
else if((type == 'unavailable') || (type == 'error')) {
|
|
// Is it me?
|
|
if(nick == getMUCNick(roomHash)) {
|
|
$(thisUser).remove();
|
|
|
|
// Disable the groupchat input
|
|
$('#' + roomHash + ' .message-area').attr('disabled', true);
|
|
|
|
// Remove all the groupchat users
|
|
$('#' + roomHash + ' .list .user').remove();
|
|
}
|
|
|
|
// Someone has been kicked or banned?
|
|
if(existArrayValue(status_code, 301) || existArrayValue(status_code, 307)) {
|
|
$(thisUser).remove();
|
|
notify = true;
|
|
|
|
// Kicked?
|
|
if(existArrayValue(status_code, 307))
|
|
write += _e("has been kicked");
|
|
|
|
// Banned?
|
|
if(existArrayValue(status_code, 301))
|
|
write += _e("has been banned");
|
|
|
|
// Any reason?
|
|
if(reason)
|
|
write += ' (' + filterThisMessage(reason, nick_html, true) + ')';
|
|
else
|
|
write += ' (' + _e("no reason") + ')';
|
|
}
|
|
|
|
// Nickname change?
|
|
else if(existArrayValue(status_code, 303) && iNick) {
|
|
notify = true;
|
|
write += printf(_e("changed his/her nickname to %s"), iNick.htmlEnc());
|
|
|
|
// New values
|
|
var new_xid = cutResource(from) + '/' + iNick;
|
|
var new_hash = hex_md5(new_xid);
|
|
var new_class = 'user ' + new_hash;
|
|
|
|
if($(thisUser).hasClass('myself'))
|
|
new_class += ' myself';
|
|
|
|
// Die the click event
|
|
$(thisUser).off('click');
|
|
|
|
// Change to the new nickname
|
|
$(thisUser).attr('data-nick', escape(iNick))
|
|
.attr('data-xid', new_xid)
|
|
.find('.name').text(iNick);
|
|
|
|
// Change the user class
|
|
$(thisUser).attr('class', new_class);
|
|
|
|
// New click event
|
|
$('#page-engine #' + roomHash + ' .list .' + new_hash).on('click', function() {
|
|
checkChatCreate(new_xid, 'private');
|
|
});
|
|
}
|
|
|
|
// We tell the user that someone left the room
|
|
else if(!initial) {
|
|
$(thisUser).remove();
|
|
notify = true;
|
|
write += _e("left the chat room");
|
|
|
|
// Any status?
|
|
if(status)
|
|
write += ' (' + filterThisMessage(status, nick_html, true) + ')';
|
|
else
|
|
write += ' (' + _e("no status") + ')';
|
|
}
|
|
|
|
// Disable the private chat input
|
|
thisPrivate.attr('disabled', true);
|
|
}
|
|
|
|
// Must notify something
|
|
if(notify)
|
|
displayMessage('groupchat', from, roomHash, nick_html, write, messageTime, getTimeStamp(), 'system-message', false);
|
|
|
|
// Set the good status show icon
|
|
switch(show) {
|
|
case 'chat':
|
|
case 'away':
|
|
case 'xa':
|
|
case 'dnd':
|
|
break;
|
|
|
|
default:
|
|
show = 'available';
|
|
break;
|
|
}
|
|
|
|
$(thisUser + ' .name').attr('class', 'name talk-images ' + show);
|
|
|
|
// Set the good status text
|
|
var uTitle = nick;
|
|
|
|
// Any XID to add?
|
|
if(iXID)
|
|
uTitle += ' (' + iXID + ')';
|
|
|
|
// Any status to add?
|
|
if(status)
|
|
uTitle += ' - ' + status;
|
|
|
|
$(thisUser).attr('title', uTitle);
|
|
|
|
// Show or hide the role category, depending of its content
|
|
$('#' + roomHash + ' .list .role').each(function() {
|
|
if($(this).find('.user').size())
|
|
$(this).show();
|
|
else
|
|
$(this).hide();
|
|
});
|
|
}
|
|
|
|
// Filters a given status
|
|
function filterStatus(xid, status, cut) {
|
|
var dStatus = '';
|
|
|
|
if(!status)
|
|
status = '';
|
|
|
|
else {
|
|
if(cut)
|
|
dStatus = truncate(status, 50);
|
|
else
|
|
dStatus = status;
|
|
|
|
dStatus = filterThisMessage(dStatus, getBuddyName(xid).htmlEnc(), true);
|
|
}
|
|
|
|
return dStatus;
|
|
}
|
|
|
|
// Displays a user's presence
|
|
function displayPresence(value, type, show, status, hash, xid, avatar, checksum, caps) {
|
|
// Display the presence in the roster
|
|
var path = '#buddy-list .' + hash;
|
|
var buddy = $('#buddy-list .content .' + hash);
|
|
var dStatus = filterStatus(xid, status, false);
|
|
var tStatus = encodeQuotes(status);
|
|
var biStatus;
|
|
|
|
// The buddy presence behind his name
|
|
$(path + ' .name .buddy-presence').replaceWith('<p class="buddy-presence talk-images ' + type + '">' + value + '</p>');
|
|
|
|
// The buddy presence in the buddy infos
|
|
if(dStatus)
|
|
biStatus = dStatus;
|
|
else
|
|
biStatus = value;
|
|
|
|
$(path + ' .bi-status').replaceWith('<p class="bi-status talk-images ' + type + '" title="' + tStatus + '">' + biStatus + '</p>');
|
|
|
|
// When the buddy disconnect himself, we hide him
|
|
if((type == 'unavailable') || (type == 'error')) {
|
|
// Set a special class to the buddy
|
|
buddy.addClass('hidden-buddy');
|
|
|
|
// No filtering is launched?
|
|
if(!SEARCH_FILTERED)
|
|
buddy.hide();
|
|
|
|
// All the buddies are shown?
|
|
if(BLIST_ALL)
|
|
buddy.show();
|
|
|
|
// Chat stuffs
|
|
if(exists('#' + hash)) {
|
|
// Remove the chatstate stuffs
|
|
resetChatState(hash);
|
|
$('#' + hash + ' .chatstate').remove();
|
|
$('#' + hash + ' .message-area').removeAttr('data-chatstates');
|
|
|
|
// Get the buddy avatar (only if a chat is opened)
|
|
getAvatar(xid, 'cache', 'true', 'forget');
|
|
}
|
|
}
|
|
|
|
// If the buddy is online
|
|
else {
|
|
// When the buddy is online, we show it
|
|
buddy.removeClass('hidden-buddy');
|
|
|
|
// No filtering is launched?
|
|
if(!SEARCH_FILTERED)
|
|
buddy.show();
|
|
|
|
// Get the online buddy avatar if not a gateway
|
|
getAvatar(xid, 'cache', avatar, checksum);
|
|
}
|
|
|
|
// Display the presence in the chat
|
|
if(exists('#' + hash)) {
|
|
// We generate a well formed status message
|
|
if(dStatus) {
|
|
// No need to write the same status two times
|
|
if(dStatus == value)
|
|
dStatus = '';
|
|
else
|
|
dStatus = ' (' + dStatus + ')';
|
|
}
|
|
|
|
// We show the presence value
|
|
$('#' + hash + ' .bc-infos').replaceWith('<p class="bc-infos" title="' + tStatus + '"><span class="' + type + ' show talk-images">' + value + '</span>' + dStatus + '</p>');
|
|
|
|
// Process the new status position
|
|
adaptChatPresence(hash);
|
|
|
|
// Get the disco#infos for this user
|
|
var highest = highestPriority(xid);
|
|
|
|
if(highest)
|
|
getDiscoInfos(highest, caps);
|
|
else
|
|
displayDiscoInfos(xid, '');
|
|
}
|
|
|
|
// Display the presence in the switcher
|
|
if(exists('#page-switch .' + hash))
|
|
$('#page-switch .' + hash + ' .icon').removeClass('available unavailable error away busy').addClass(type);
|
|
|
|
// Update roster groups
|
|
if(!SEARCH_FILTERED)
|
|
updateGroups();
|
|
else
|
|
funnelFilterBuddySearch();
|
|
}
|
|
|
|
// Process the chat presence position
|
|
function adaptChatPresence(hash) {
|
|
// Get values
|
|
var pep_numb = $('#' + hash + ' .bc-pep').find('a').size();
|
|
|
|
// Process the left/right position
|
|
var presence_h = 12;
|
|
|
|
if(pep_numb)
|
|
presence_h = (pep_numb * 20) + 18;
|
|
|
|
// Apply the left/right position
|
|
var presence_h_tag = ($('html').attr('dir') == 'rtl') ? 'left' : 'right';
|
|
$('#' + hash + ' p.bc-infos').css(presence_h_tag, presence_h);
|
|
}
|
|
|
|
// Convert the presence "show" element into a human-readable output
|
|
function humanShow(show, type) {
|
|
if(type == 'unavailable')
|
|
show = _e("Unavailable");
|
|
|
|
else if(type == 'error')
|
|
show = _e("Error");
|
|
|
|
else {
|
|
switch(show) {
|
|
case 'chat':
|
|
show = _e("Talkative");
|
|
break;
|
|
|
|
case 'away':
|
|
show = _e("Away");
|
|
break;
|
|
|
|
case 'xa':
|
|
show = _e("Not available");
|
|
break;
|
|
|
|
case 'dnd':
|
|
show = _e("Busy");
|
|
break;
|
|
|
|
default:
|
|
show = _e("Available");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return show;
|
|
}
|
|
|
|
// Makes the presence data go in the right way
|
|
function presenceIA(type, show, status, hash, xid, avatar, checksum, caps) {
|
|
// Is there a status defined?
|
|
if(!status)
|
|
status = humanShow(show, type);
|
|
|
|
// Then we can handle the events
|
|
if(type == 'error')
|
|
displayPresence(_e("Error"), 'error', show, status, hash, xid, avatar, checksum, caps);
|
|
|
|
else if(type == 'unavailable')
|
|
displayPresence(_e("Unavailable"), 'unavailable', show, status, hash, xid, avatar, checksum, caps);
|
|
|
|
else {
|
|
switch(show) {
|
|
case 'chat':
|
|
displayPresence(_e("Talkative"), 'available', show, status, hash, xid, avatar, checksum, caps);
|
|
break;
|
|
|
|
case 'away':
|
|
displayPresence(_e("Away"), 'away', show, status, hash, xid, avatar, checksum, caps);
|
|
break;
|
|
|
|
case 'xa':
|
|
displayPresence(_e("Not available"), 'busy', show, status, hash, xid, avatar, checksum, caps);
|
|
break;
|
|
|
|
case 'dnd':
|
|
displayPresence(_e("Busy"), 'busy', show, status, hash, xid, avatar, checksum, caps);
|
|
break;
|
|
|
|
default:
|
|
displayPresence(_e("Available"), 'available', show, status, hash, xid, avatar, checksum, caps);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush the presence data for a given user
|
|
function flushPresence(xid) {
|
|
var flushed_marker = false;
|
|
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored presence
|
|
if(explodeThis('_', current, 0) == 'presence') {
|
|
// Get the current XID
|
|
var now_full = explodeThis('_', current, 1);
|
|
var now_bare = bareXID(now_full);
|
|
|
|
// If the current XID equals the asked XID
|
|
if(now_bare == xid) {
|
|
if(removeDB('presence-stanza', now_full)) {
|
|
logThis('Presence data flushed for: ' + now_full, 3);
|
|
|
|
flushed_marker = true;
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return flushed_marker;
|
|
}
|
|
|
|
// Process the highest resource priority for an user
|
|
function processPriority(xid, resource, resources_obj) {
|
|
try {
|
|
if(!xid) {
|
|
logThis('processPriority > No XID value', 2);
|
|
return;
|
|
}
|
|
|
|
// Initialize vars
|
|
var cur_resource, cur_from, cur_pr,
|
|
cur_xml, cur_priority,
|
|
from_highest, from_highest;
|
|
|
|
from_highest = null;
|
|
max_priority = null;
|
|
|
|
// Groupchat presence? (no priority here)
|
|
if(xid.indexOf('/') !== -1) {
|
|
from_highest = xid;
|
|
|
|
logThis('Processed presence for groupchat user: ' + xid);
|
|
} else {
|
|
if(!highestPriority(xid)) {
|
|
from_highest = xid + '/' + resource;
|
|
|
|
logThis('Processed initial presence for regular user: ' + xid + ' (highest priority for: ' + (from_highest || 'none') + ')');
|
|
} else {
|
|
for(cur_resource in resources_obj) {
|
|
// Read presence data
|
|
cur_from = xid + '/' + cur_resource;
|
|
cur_pr = getDB('presence-stanza', cur_from);
|
|
|
|
if(cur_pr) {
|
|
// Parse presence data
|
|
cur_xml = XMLFromString(cur_pr);
|
|
cur_priority = $(cur_xml).find('priority').text();
|
|
cur_priority = !isNaN(cur_priority) ? parseInt(cur_priority) : 0;
|
|
|
|
// Higher priority?
|
|
if((cur_priority >= max_priority) || (max_priority == null)) {
|
|
max_priority = cur_priority;
|
|
from_highest = cur_from;
|
|
}
|
|
}
|
|
}
|
|
|
|
logThis('Processed presence for regular user: ' + xid + ' (highest priority for: ' + (from_highest || 'none') + ')');
|
|
}
|
|
}
|
|
|
|
if(from_highest)
|
|
setDB('presence-priority', xid, from_highest);
|
|
else
|
|
removeDB('presence-priority', xid);
|
|
} catch(e) {
|
|
logThis('Error on presence processing: ' + e, 1);
|
|
}
|
|
}
|
|
|
|
// Returns the highest presence priority XID for an user
|
|
function highestPriority(xid) {
|
|
return getDB('presence-priority', xid) || '';
|
|
}
|
|
|
|
// Gets the resource from a XID which has the highest priority
|
|
function highestPriorityStanza(xid) {
|
|
var pr;
|
|
var highest = highestPriority(xid);
|
|
|
|
if(highest) pr = getDB('presence-stanza', highest);
|
|
if(!pr) pr = '<presence><type>unavailable</type></presence>';
|
|
|
|
return XMLFromString(pr);
|
|
}
|
|
|
|
// Lists presence resources for an user
|
|
function resourcesPresence(xid) {
|
|
try {
|
|
var resources_obj = {};
|
|
var resources_db = getDB('presence-resources', xid);
|
|
|
|
if(resources_db) {
|
|
resources_obj = $.evalJSON(resources_db);
|
|
}
|
|
|
|
return resources_obj;
|
|
} catch(e) {}
|
|
}
|
|
|
|
// Adds a given presence resource for an user
|
|
function addResourcePresence(xid, resource) {
|
|
try {
|
|
var resources_obj = resourcesPresence(xid);
|
|
|
|
resources_obj[resource] = 1;
|
|
setDB('presence-resources', xid, $.toJSON(resources_obj));
|
|
|
|
return resources_obj;
|
|
} catch(e) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Removes a given presence resource for an user
|
|
function removeResourcePresence(xid, resource) {
|
|
try {
|
|
var resources_obj = resourcesPresence(xid);
|
|
|
|
delete resources_obj[resource];
|
|
setDB('presence-resources', xid, $.toJSON(resources_obj));
|
|
|
|
return resources_obj;
|
|
} catch(e) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Makes something easy to process for the presence IA
|
|
function presenceFunnel(xid, hash) {
|
|
// Get the highest priority presence value
|
|
var xml = $(highestPriorityStanza(xid));
|
|
var type = xml.find('type').text();
|
|
var show = xml.find('show').text();
|
|
var status = xml.find('status').text();
|
|
var avatar = xml.find('avatar').text();
|
|
var checksum = xml.find('checksum').text();
|
|
var caps = xml.find('caps').text();
|
|
|
|
// Display the presence with that stored value
|
|
if(!type && !show)
|
|
presenceIA('', 'available', status, hash, xid, avatar, checksum, caps);
|
|
else
|
|
presenceIA(type, show, status, hash, xid, avatar, checksum, caps);
|
|
}
|
|
|
|
// Sends a defined presence packet
|
|
function sendPresence(to, type, show, status, checksum, limit_history, password, handle) {
|
|
// Get some stuffs
|
|
var priority = getDB('priority', 1);
|
|
|
|
if(!priority)
|
|
priority = '1';
|
|
if(!checksum)
|
|
checksum = getDB('checksum', 1);
|
|
if(show == 'available')
|
|
show = '';
|
|
if(type == 'available')
|
|
type = '';
|
|
|
|
// New presence
|
|
var presence = new JSJaCPresence();
|
|
|
|
// Avoid "null" or "none" if nothing stored
|
|
if(!checksum || (checksum == 'none'))
|
|
checksum = '';
|
|
|
|
// Presence headers
|
|
if(to)
|
|
presence.setTo(to);
|
|
if(type)
|
|
presence.setType(type);
|
|
if(show)
|
|
presence.setShow(show);
|
|
if(status)
|
|
presence.setStatus(status);
|
|
|
|
presence.setPriority(priority);
|
|
|
|
// CAPS (entity capabilities)
|
|
presence.appendNode('c', {'xmlns': NS_CAPS, 'hash': 'sha-1', 'node': 'http://jappix.org/', 'ver': myCaps()});
|
|
|
|
// Nickname
|
|
var nickname = getName();
|
|
|
|
if(nickname && !limit_history)
|
|
presence.appendNode('nick', {'xmlns': NS_NICK}, nickname);
|
|
|
|
// vcard-temp:x:update node
|
|
var x = presence.appendNode('x', {'xmlns': NS_VCARD_P});
|
|
x.appendChild(presence.buildNode('photo', {'xmlns': NS_VCARD_P}, checksum));
|
|
|
|
// MUC X data
|
|
if(limit_history || password) {
|
|
var xMUC = presence.appendNode('x', {'xmlns': NS_MUC});
|
|
|
|
// Max messages age (for MUC)
|
|
if(limit_history)
|
|
xMUC.appendChild(presence.buildNode('history', {'maxstanzas': 20, 'seconds': 86400, 'xmlns': NS_MUC}));
|
|
|
|
// Room password
|
|
if(password)
|
|
xMUC.appendChild(presence.buildNode('password', {'xmlns': NS_MUC}, password));
|
|
}
|
|
|
|
// If away, send a last activity time
|
|
if((show == 'away') || (show == 'xa')) {
|
|
/* REF: http://xmpp.org/extensions/xep-0256.html */
|
|
|
|
presence.appendNode(presence.buildNode('query', {
|
|
'xmlns': NS_LAST,
|
|
'seconds': getPresenceLast()
|
|
}));
|
|
}
|
|
|
|
// Else, set a new last activity stamp
|
|
else
|
|
PRESENCE_LAST_ACTIVITY = getTimeStamp();
|
|
|
|
// Send the presence packet
|
|
if(handle)
|
|
con.send(presence, handle);
|
|
else
|
|
con.send(presence);
|
|
|
|
if(!type)
|
|
type = 'available';
|
|
|
|
logThis('Presence sent: ' + type, 3);
|
|
}
|
|
|
|
// Performs all the actions to get the presence data
|
|
function presenceSend(checksum, autoidle) {
|
|
// We get the values of the inputs
|
|
var show = getUserShow();
|
|
var status = getUserStatus();
|
|
|
|
// Send the presence
|
|
if(!isAnonymous())
|
|
sendPresence('', '', show, status, checksum);
|
|
|
|
// We set the good icon
|
|
presenceIcon(show);
|
|
|
|
// We store our presence
|
|
if(!autoidle)
|
|
setDB('presence-show', 1, show);
|
|
|
|
// We send the presence to our active MUC
|
|
$('.page-engine-chan[data-type="groupchat"]').each(function() {
|
|
var tmp_nick = $(this).attr('data-nick');
|
|
|
|
if(!tmp_nick)
|
|
return;
|
|
|
|
var room = unescape($(this).attr('data-xid'));
|
|
var nick = unescape(tmp_nick);
|
|
|
|
// Must re-initialize?
|
|
if(RESUME)
|
|
getMUC(room, nick);
|
|
|
|
// Not disabled?
|
|
else if(!$(this).find('.message-area').attr('disabled'))
|
|
sendPresence(room + '/' + nick, '', show, status, '', true);
|
|
});
|
|
}
|
|
|
|
// Changes the presence icon
|
|
function presenceIcon(value) {
|
|
$('#my-infos .f-presence a.picker').attr('data-value', value);
|
|
}
|
|
|
|
// Sends a subscribe stanza
|
|
function sendSubscribe(to, type) {
|
|
var status = '';
|
|
|
|
// Subscribe request?
|
|
if(type == 'subscribe')
|
|
status = printf(_e("Hi, I am %s, I would like to add you as my friend."), getName());
|
|
|
|
sendPresence(to, type, '', status);
|
|
}
|
|
|
|
// Accepts the subscription from another entity
|
|
function acceptSubscribe(xid, name) {
|
|
// We update our chat
|
|
$('#' + hex_md5(xid) + ' .tools-add').hide();
|
|
|
|
// We send a subsribed presence (to confirm)
|
|
sendSubscribe(xid, 'subscribed');
|
|
|
|
// We send a subscription request (subscribe both sides)
|
|
sendSubscribe(xid, 'subscribe');
|
|
|
|
// Specify the buddy name (if any)
|
|
if(name)
|
|
sendRoster(xid, '', name)
|
|
}
|
|
|
|
// Sends automatic away presence
|
|
var AUTO_IDLE = false;
|
|
|
|
function autoIdle() {
|
|
// Not connected?
|
|
if(!isConnected())
|
|
return;
|
|
|
|
// Stop if an xa presence was set manually
|
|
var last_presence = getUserShow();
|
|
|
|
if(!AUTO_IDLE && ((last_presence == 'away') || (last_presence == 'xa')))
|
|
return;
|
|
|
|
var idle_presence;
|
|
var activity_limit;
|
|
|
|
// Can we extend to auto extended away mode (20 minutes)?
|
|
if(AUTO_IDLE && (last_presence == 'away')) {
|
|
idle_presence = 'xa';
|
|
activity_limit = 1200;
|
|
}
|
|
|
|
// We must set the user to auto-away (10 minutes)
|
|
else {
|
|
idle_presence = 'away';
|
|
activity_limit = 600;
|
|
}
|
|
|
|
// The user is really inactive and has set another presence than extended away
|
|
if(((!AUTO_IDLE && (last_presence != 'away')) || (AUTO_IDLE && (last_presence == 'away'))) && (getLastActivity() >= activity_limit)) {
|
|
// Then tell we use an auto presence
|
|
AUTO_IDLE = true;
|
|
|
|
// Get the old status message
|
|
var status = getDB('options', 'presence-status');
|
|
|
|
if(!status)
|
|
status = '';
|
|
|
|
// Change the presence input
|
|
$('#my-infos .f-presence a.picker').attr('data-value', idle_presence);
|
|
$('#presence-status').val(status);
|
|
|
|
// Then send the xa presence
|
|
presenceSend('', true);
|
|
|
|
logThis('Auto-idle presence sent: ' + idle_presence, 3);
|
|
}
|
|
}
|
|
|
|
// Restores the old presence on a document bind
|
|
function eventIdle() {
|
|
// If we were idle, restore our old presence
|
|
if(AUTO_IDLE) {
|
|
// Get the values
|
|
var show = getDB('presence-show', 1);
|
|
var status = getDB('options', 'presence-status');
|
|
|
|
// Change the presence input
|
|
$('#my-infos .f-presence a.picker').attr('data-value', show);
|
|
$('#presence-status').val(status);
|
|
$('#presence-status').placeholder();
|
|
|
|
// Then restore the old presence
|
|
presenceSend('', true);
|
|
|
|
if(!show)
|
|
show = 'available';
|
|
|
|
logThis('Presence restored: ' + show, 3);
|
|
}
|
|
|
|
// Apply some values
|
|
AUTO_IDLE = false;
|
|
LAST_ACTIVITY = getTimeStamp();
|
|
}
|
|
|
|
// Lives the auto idle functions
|
|
function liveIdle() {
|
|
// Apply the autoIdle function every minute
|
|
AUTO_IDLE = false;
|
|
$('#my-infos .f-presence').everyTime('30s', autoIdle);
|
|
|
|
// On body bind (click & key event)
|
|
$('body').on('mousedown', eventIdle)
|
|
.on('mousemove', eventIdle)
|
|
.on('keydown', eventIdle);
|
|
}
|
|
|
|
// Kills the auto idle functions
|
|
function dieIdle() {
|
|
// Remove the event detector
|
|
$('body').off('mousedown', eventIdle)
|
|
.off('mousemove', eventIdle)
|
|
.off('keydown', eventIdle);
|
|
}
|
|
|
|
// Gets the user presence show
|
|
function getUserShow() {
|
|
return $('#my-infos .f-presence a.picker').attr('data-value');
|
|
}
|
|
|
|
// Gets the user presence status
|
|
function getUserStatus() {
|
|
return $('#presence-status').val();
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchPresence() {
|
|
// Click event for user presence show
|
|
$('#my-infos .f-presence a.picker').click(function() {
|
|
// Disabled?
|
|
if($(this).hasClass('disabled'))
|
|
return false;
|
|
|
|
// Initialize some vars
|
|
var path = '#my-infos .f-presence div.bubble';
|
|
var show_id = ['xa', 'away', 'available'];
|
|
var show_lang = [_e("Not available"), _e("Away"), _e("Available")];
|
|
var show_val = getUserShow();
|
|
|
|
// Yet displayed?
|
|
var can_append = true;
|
|
|
|
if(exists(path))
|
|
can_append = false;
|
|
|
|
// Add this bubble!
|
|
showBubble(path);
|
|
|
|
if(!can_append)
|
|
return false;
|
|
|
|
// Generate the HTML code
|
|
var html = '<div class="bubble removable">';
|
|
|
|
for(i in show_id) {
|
|
// Yet in use: no need to display it!
|
|
if(show_id[i] == show_val)
|
|
continue;
|
|
|
|
html += '<a href="#" class="talk-images" data-value="' + show_id[i] + '" title="' + show_lang[i] + '"></a>';
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
// Append the HTML code
|
|
$('#my-infos .f-presence').append(html);
|
|
|
|
// Click event
|
|
$(path + ' a').click(function() {
|
|
// Update the presence show marker
|
|
$('#my-infos .f-presence a.picker').attr('data-value', $(this).attr('data-value'));
|
|
|
|
// Close the bubble
|
|
closeBubbles();
|
|
|
|
// Focus on the status input
|
|
$(document).oneTime(10, function() {
|
|
$('#presence-status').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
// Submit events for user presence status
|
|
$('#presence-status').placeholder()
|
|
|
|
.keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
$(this).blur();
|
|
|
|
return false;
|
|
}
|
|
})
|
|
|
|
.blur(function() {
|
|
// Read the parameters
|
|
var show = getUserShow();
|
|
var status = getUserStatus();
|
|
|
|
// Read the old parameters
|
|
var old_show = getDB('presence-show', 1);
|
|
var old_status = getDB('options', 'presence-status');
|
|
|
|
// Must send the presence?
|
|
if((show != old_show) || (status != old_status)) {
|
|
// Update the local stored status
|
|
setDB('options', 'presence-status', status);
|
|
|
|
// Update the server stored status
|
|
if(status != old_status)
|
|
storeOptions();
|
|
|
|
// Send the presence
|
|
presenceSend();
|
|
}
|
|
})
|
|
|
|
// Input focus handler
|
|
.focus(function() {
|
|
closeBubbles();
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the roster JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 08/08/12
|
|
|
|
*/
|
|
|
|
// Gets the roster items
|
|
function getRoster() {
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setType('get');
|
|
iq.setQuery(NS_ROSTER);
|
|
|
|
con.send(iq, handleRoster);
|
|
}
|
|
|
|
// Handles the roster items
|
|
function handleRoster(iq) {
|
|
// Parse the roster xml
|
|
$(iq.getQuery()).find('item').each(function() {
|
|
// Get user data
|
|
var self = $(this);
|
|
var user_xid = self.attr('jid');
|
|
var user_subscription = self.attr('subscription');
|
|
|
|
// Parse roster data & display user
|
|
parseRoster($(this), 'load');
|
|
|
|
// Request user microblog (populates channel)
|
|
if(user_xid && ((user_subscription == 'both') || (user_subscription == 'to')))
|
|
requestMicroblog(user_xid, 1, null, handleRosterMicroblog);
|
|
});
|
|
|
|
// Update our avatar (if changed), and send our presence
|
|
getAvatar(getXID(), 'force', 'true', 'forget');
|
|
|
|
logThis('Roster received.');
|
|
}
|
|
|
|
// Parses the group XML and display the roster
|
|
function parseRoster(current, mode) {
|
|
// Get the values
|
|
xid = current.attr('jid');
|
|
dName = current.attr('name');
|
|
subscription = current.attr('subscription');
|
|
xidHash = hex_md5(xid);
|
|
|
|
// Create an array containing the groups
|
|
var groups = new Array();
|
|
|
|
current.find('group').each(function() {
|
|
var group_text = $(this).text();
|
|
|
|
if(group_text)
|
|
groups.push(group_text);
|
|
});
|
|
|
|
// No group?
|
|
if(!groups.length)
|
|
groups.push(_e("Unclassified"));
|
|
|
|
// If no name is defined, we get the default nick of the buddy
|
|
if(!dName)
|
|
dName = getXIDNick(xid);
|
|
|
|
displayRoster(xid, xidHash, dName, subscription, groups, mode);
|
|
}
|
|
|
|
// Updates the roster groups
|
|
function updateGroups() {
|
|
$('#buddy-list .one-group').each(function() {
|
|
// Current values
|
|
var check = $(this).find('.buddy').size();
|
|
var hidden = $(this).find('.buddy:not(.hidden-buddy:hidden)').size();
|
|
|
|
// Special case: the filtering tool
|
|
if(SEARCH_FILTERED)
|
|
hidden = $(this).find('.buddy:visible').size();
|
|
|
|
// If the group is empty
|
|
if(!check)
|
|
$(this).remove();
|
|
|
|
// If the group contains no online buddy (and is not just hidden)
|
|
if(!hidden && $(this).find('a.group').hasClass('minus'))
|
|
$(this).hide();
|
|
else
|
|
$(this).show();
|
|
});
|
|
}
|
|
|
|
// Displays a defined roster item
|
|
function displayRoster(dXID, dXIDHash, dName, dSubscription, dGroup, dMode) {
|
|
// First remove the buddy
|
|
$('#buddy-list .' + dXIDHash).remove();
|
|
|
|
// Define some things around the groups
|
|
var is_gateway = isGateway(dXID);
|
|
var gateway = '';
|
|
|
|
if(is_gateway) {
|
|
gateway = ' gateway';
|
|
dGroup = new Array(_e("Gateways"));
|
|
}
|
|
|
|
// Remove request
|
|
if(dSubscription == 'remove') {
|
|
// Flush presence
|
|
flushPresence(dXID);
|
|
presenceFunnel(dXID, dXIDHash);
|
|
|
|
// Empty social channel
|
|
$('#channel .mixed .one-update.update_' + dXIDHash).remove();
|
|
}
|
|
|
|
// Other request
|
|
else {
|
|
// Is this buddy blocked?
|
|
var privacy_class = '';
|
|
var privacy_state = statusPrivacy('block', dXID);
|
|
|
|
if(privacy_state == 'deny')
|
|
privacy_class = ' blocked';
|
|
|
|
// For each group this buddy has
|
|
for(i in dGroup) {
|
|
var cGroup = dGroup[i];
|
|
|
|
if(cGroup) {
|
|
// Process some vars
|
|
var groupHash = 'group' + hex_md5(cGroup);
|
|
var groupContent = '#buddy-list .' + groupHash;
|
|
var groupBuddies = groupContent + ' .group-buddies';
|
|
|
|
// Is this group blocked?
|
|
if((statusPrivacy('block', cGroup) == 'deny') && (privacy_state != 'allow'))
|
|
privacy_class = ' blocked';
|
|
|
|
// Group not yet displayed
|
|
if(!exists(groupContent)) {
|
|
// Define some things
|
|
var groupCont = '#buddy-list .content';
|
|
var groupToggle = groupCont + ' .' + groupHash + ' a.group';
|
|
|
|
// Create the HTML markup of the group
|
|
$(groupCont).prepend(
|
|
'<div class="' + groupHash + ' one-group" data-group="' + escape(cGroup) + '">' +
|
|
'<a href="#" class="group talk-images minus">' + cGroup.htmlEnc() + '</a>' +
|
|
'<div class="group-buddies"></div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Create the click event which will hide and show the content
|
|
$(groupToggle).click(function() {
|
|
var group = $(groupBuddies);
|
|
var group_toggle = $(groupContent + ' a.group');
|
|
|
|
// We must hide the buddies
|
|
if(group_toggle.hasClass('minus')) {
|
|
group.hide();
|
|
group_toggle.removeClass('minus').addClass('plus');
|
|
|
|
// Remove the group opened buddy-info
|
|
closeBubbles();
|
|
}
|
|
|
|
// We must show the buddies
|
|
else {
|
|
group_toggle.removeClass('plus').addClass('minus');
|
|
group.show();
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Initialize the HTML code
|
|
var name_code = '<p class="buddy-name">' + dName.htmlEnc() + '</p>';
|
|
var presence_code = '<p class="buddy-presence talk-images unavailable">' + _e("Unavailable") + '</p>';
|
|
|
|
var html = '<div class="hidden-buddy buddy ibubble ' + dXIDHash + gateway + privacy_class + '" data-xid="' + escape(dXID) + '">' +
|
|
'<div class="buddy-click">';
|
|
|
|
// Display avatar if not gateway
|
|
if(!is_gateway)
|
|
html += '<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>';
|
|
|
|
html += '<div class="name">';
|
|
|
|
// Special gateway code
|
|
if(is_gateway)
|
|
html += presence_code +
|
|
name_code;
|
|
|
|
else
|
|
html += name_code +
|
|
presence_code;
|
|
|
|
html += '</div></div></div>';
|
|
|
|
// Create the DOM element for this buddy
|
|
$(groupBuddies).append(html);
|
|
|
|
// Apply the hover event
|
|
applyBuddyHover(dXID, dXIDHash, dName, dSubscription, dGroup, groupHash);
|
|
}
|
|
}
|
|
|
|
// Click event on this buddy
|
|
$('#buddy-list .' + dXIDHash + ' .buddy-click').click(function() {
|
|
return checkChatCreate(dXID, 'chat');
|
|
});
|
|
|
|
// We get the user presence if necessary
|
|
if(dMode == 'presence')
|
|
presenceFunnel(dXID, dXIDHash);
|
|
|
|
// If the buddy must be shown
|
|
if(BLIST_ALL)
|
|
$('#buddy-list .' + dXIDHash).show();
|
|
}
|
|
|
|
// We update our groups
|
|
if(!SEARCH_FILTERED)
|
|
updateGroups();
|
|
else
|
|
funnelFilterBuddySearch();
|
|
}
|
|
|
|
// Applies the buddy editing input events
|
|
function applyBuddyInput(xid) {
|
|
// Initialize
|
|
var path = '#buddy-list .buddy[data-xid="' + escape(xid) + '"]';
|
|
var rename = path + ' .bm-rename input';
|
|
var group = path + ' .bm-group input';
|
|
var manage_infos = path + ' .manage-infos';
|
|
var bm_choose = manage_infos + ' div.bm-choose';
|
|
|
|
// Keyup events
|
|
$(rename).keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// Send the item
|
|
sendRoster(xid, '', trim($(rename).val()), thisBuddyGroups(xid));
|
|
|
|
// Remove the buddy editor
|
|
closeBubbles();
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
$(group).keyup(function(e) {
|
|
if(e.keyCode == 13) {
|
|
// Empty input?
|
|
if(!trim($(this).val())) {
|
|
// Send the item
|
|
sendRoster(xid, '', trim($(rename).val()), thisBuddyGroups(xid));
|
|
|
|
// Remove the buddy editor
|
|
closeBubbles();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Get the values
|
|
var this_value = trim($(this).val());
|
|
var escaped_value = escape(this_value);
|
|
|
|
// Check if the group yet exists
|
|
var group_exists = false;
|
|
|
|
$(bm_choose + ' label span').each(function() {
|
|
if($(this).text() == this_value)
|
|
group_exists = true;
|
|
});
|
|
|
|
// Create a new checked checkbox
|
|
if(!group_exists)
|
|
$(bm_choose).prepend('<label><input type="checkbox" data-group="' + escaped_value + '" /><span>' + this_value.htmlEnc() + '</span></label>');
|
|
|
|
// Check the checkbox
|
|
$(bm_choose + ' input[data-group="' + escaped_value + '"]').attr('checked', true);
|
|
|
|
// Reset the value of this input
|
|
$(this).val('');
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Click events
|
|
$(manage_infos + ' p.bm-authorize a.to').click(function() {
|
|
closeBubbles();
|
|
sendSubscribe(xid, 'subscribed');
|
|
|
|
return false;
|
|
});
|
|
|
|
$(manage_infos + ' p.bm-authorize a.from').click(function() {
|
|
closeBubbles();
|
|
sendSubscribe(xid, 'subscribe');
|
|
|
|
return false;
|
|
});
|
|
|
|
$(manage_infos + ' p.bm-authorize a.unblock').click(function() {
|
|
closeBubbles();
|
|
|
|
// Update privacy settings
|
|
pushPrivacy('block', ['jid'], [xid], ['allow'], [false], [true], [true], [true], '', 'roster');
|
|
$(path).removeClass('blocked');
|
|
|
|
// Enable the "block" list
|
|
changePrivacy('block', 'active');
|
|
changePrivacy('block', 'default');
|
|
|
|
// Send an available presence
|
|
sendPresence(xid, 'available', getUserShow(), getUserStatus());
|
|
|
|
return false;
|
|
});
|
|
|
|
$(manage_infos + ' p.bm-remove a.remove').click(function() {
|
|
closeBubbles();
|
|
|
|
// First unregister if gateway
|
|
if(isGateway(xid))
|
|
unregisterGatewayRoster(xid);
|
|
|
|
// Then send roster removal query
|
|
sendRoster(xid, 'remove');
|
|
|
|
return false;
|
|
});
|
|
|
|
$(manage_infos + ' p.bm-remove a.prohibit').click(function() {
|
|
closeBubbles();
|
|
sendSubscribe(xid, 'unsubscribed');
|
|
|
|
return false;
|
|
});
|
|
|
|
$(manage_infos + ' p.bm-remove a.block').click(function() {
|
|
closeBubbles();
|
|
|
|
// Update privacy settings
|
|
pushPrivacy('block', ['jid'], [xid], ['deny'], [false], [true], [true], [true], '', 'roster');
|
|
$(path).addClass('blocked');
|
|
|
|
// Enable the "block" list
|
|
changePrivacy('block', 'active');
|
|
changePrivacy('block', 'default');
|
|
|
|
// Send an unavailable presence
|
|
sendPresence(xid, 'unavailable');
|
|
|
|
// Remove the user presence
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored presence
|
|
if((explodeThis('_', current, 0) == 'presence') && (bareXID(explodeThis('_', current, 1)) == xid))
|
|
storageDB.removeItem(current);
|
|
}
|
|
|
|
// Manage his new presence
|
|
presenceFunnel(xid, hex_md5(xid));
|
|
|
|
return false;
|
|
});
|
|
|
|
$(manage_infos + ' a.save').click(function() {
|
|
// Send the item
|
|
sendRoster(xid, '', trim($(rename).val()), thisBuddyGroups(xid));
|
|
|
|
// Remove the buddy editor
|
|
closeBubbles();
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Applies the buddy editing hover events
|
|
function applyBuddyHover(xid, hash, nick, subscription, groups, group_hash) {
|
|
// Generate the values
|
|
var bPath = '#buddy-list .' + group_hash + ' .buddy[data-xid="' + escape(xid) + '"]';
|
|
var iPath = bPath + ' .buddy-infos';
|
|
|
|
// Apply the hover event
|
|
$(bPath).hover(function() {
|
|
// Another bubble exist
|
|
if(exists('#buddy-list .buddy-infos'))
|
|
return false;
|
|
|
|
$(bPath).oneTime(200, function() {
|
|
// Another bubble exist
|
|
if(exists('#buddy-list .buddy-infos'))
|
|
return false;
|
|
|
|
// Add this bubble!
|
|
showBubble(iPath);
|
|
|
|
// Create the buddy infos DOM element
|
|
$(bPath).append(
|
|
'<div class="buddy-infos bubble removable">' +
|
|
'<div class="buddy-infos-subarrow talk-images"></div>' +
|
|
'<div class="buddy-infos-subitem">' +
|
|
'<div class="pep-infos">' +
|
|
'<p class="bi-status talk-images unavailable">' + _e("unknown") + '</p>' +
|
|
'<p class="bi-mood talk-images mood-four">' + _e("unknown") + '</p>' +
|
|
'<p class="bi-activity talk-images activity-exercising">' + _e("unknown") + '</p>' +
|
|
'<p class="bi-tune talk-images tune-note">' + _e("unknown") + '</p>' +
|
|
'<p class="bi-geoloc talk-images location-world">' + _e("unknown") + '</p>' +
|
|
'<p class="bi-view talk-images view-individual"><a href="#" class="profile">' + _e("Profile") + '</a> / <a href="#" class="channel">' + _e("Channel") + '</a> / <a href="#" class="commands">' + _e("Commands") + '</a></p>' +
|
|
'<p class="bi-edit talk-images edit-buddy"><a href="#">' + _e("Edit") + '</a></p>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Sets the good position
|
|
buddyInfosPosition(xid, group_hash);
|
|
|
|
// Get the presence
|
|
presenceFunnel(xid, hash);
|
|
|
|
// Get the PEP infos
|
|
displayAllPEP(xid);
|
|
|
|
// Click events
|
|
$(bPath + ' .bi-view a').click(function() {
|
|
// Renitialize the buddy infos
|
|
closeBubbles();
|
|
|
|
// Profile
|
|
if($(this).is('.profile'))
|
|
openUserInfos(xid);
|
|
|
|
// Channel
|
|
else if($(this).is('.channel'))
|
|
fromInfosMicroblog(xid, hash);
|
|
|
|
// Command
|
|
else if($(this).is('.commands'))
|
|
retrieveAdHoc(xid);
|
|
|
|
return false;
|
|
});
|
|
|
|
$(bPath + ' .bi-edit a').click(function() {
|
|
buddyEdit(xid, nick, subscription, groups);
|
|
|
|
return false;
|
|
});
|
|
});
|
|
}, function() {
|
|
if(!exists(iPath + ' .manage-infos'))
|
|
closeBubbles();
|
|
|
|
$(bPath).stopTime();
|
|
});
|
|
}
|
|
|
|
// Sets the good buddy-infos position
|
|
function buddyInfosPosition(xid, group_hash) {
|
|
// Paths
|
|
var group = '#buddy-list .' + group_hash;
|
|
var buddy = group + ' .buddy[data-xid="' + escape(xid) + '"]';
|
|
var buddy_infos = buddy + ' .buddy-infos';
|
|
|
|
// Get the offset to define
|
|
var offset = 3;
|
|
|
|
if(isGateway(xid))
|
|
offset = -8;
|
|
|
|
// Process the position
|
|
var v_position = $(buddy).position().top + offset;
|
|
var h_position = $(buddy).width() - 10;
|
|
|
|
// Apply the top position
|
|
$(buddy_infos).css('top', v_position);
|
|
|
|
// Apply the left/right position
|
|
if($('html').attr('dir') == 'rtl')
|
|
$(buddy_infos).css('right', h_position);
|
|
else
|
|
$(buddy_infos).css('left', h_position);
|
|
}
|
|
|
|
// Generates an array of the current groups of a buddy
|
|
function thisBuddyGroups(xid) {
|
|
var path = '#buddy-list .buddy[data-xid="' + escape(xid) + '"] ';
|
|
var array = new Array();
|
|
|
|
// Each checked checkboxes
|
|
$(path + 'div.bm-choose input[type="checkbox"]').filter(':checked').each(function() {
|
|
array.push(unescape($(this).attr('data-group')));
|
|
});
|
|
|
|
// Entered input value (and not yet in the array)
|
|
var value = trim($(path + 'p.bm-group input').val());
|
|
|
|
if(value && !existArrayValue(array, value))
|
|
array.push(value);
|
|
|
|
return array;
|
|
}
|
|
|
|
// Adds a given contact to our roster
|
|
function addThisContact(xid, name) {
|
|
logThis('Add this contact: ' + xid + ', as ' + name, 3);
|
|
|
|
// Cut the resource of this XID
|
|
xid = bareXID(xid);
|
|
|
|
// If the form is complete
|
|
if(xid) {
|
|
// We send the subscription
|
|
sendSubscribe(xid, 'subscribe');
|
|
sendRoster(xid, '', name);
|
|
|
|
// We hide the bubble
|
|
closeBubbles();
|
|
}
|
|
}
|
|
|
|
// Gets an array of all the groups in the roster
|
|
function getAllGroups() {
|
|
var groups = new Array();
|
|
|
|
$('#buddy-list .one-group').each(function() {
|
|
var current = unescape($(this).attr('data-group'));
|
|
|
|
if((current != _e("Unclassified")) && (current != _e("Gateways")))
|
|
groups.push(current);
|
|
});
|
|
|
|
return groups.sort();
|
|
}
|
|
|
|
// Edits buddy informations
|
|
function buddyEdit(xid, nick, subscription, groups) {
|
|
logThis('Buddy edit: ' + xid, 3);
|
|
|
|
// Initialize
|
|
var path = '#buddy-list .buddy[data-xid="' + escape(xid) + '"] .';
|
|
var html = '<div class="manage-infos">';
|
|
|
|
// Get the privacy state
|
|
var privacy_state = statusPrivacy('block', xid);
|
|
var privacy_active = getDB('privacy-marker', 'available');
|
|
|
|
// Get the group privacy state
|
|
for(g in groups) {
|
|
if((statusPrivacy('block', groups[g]) == 'deny') && (privacy_state != 'allow'))
|
|
privacy_state = 'deny';
|
|
}
|
|
|
|
// The subscription with this buddy is not full
|
|
if((subscription != 'both') || ((privacy_state == 'deny') && privacy_active)) {
|
|
var authorize_links = '';
|
|
html += '<p class="bm-authorize talk-images">';
|
|
|
|
// Link to allow to see our status
|
|
if((subscription == 'to') || (subscription == 'none'))
|
|
authorize_links += '<a href="#" class="to">' + _e("Authorize") + '</a>';
|
|
|
|
// Link to ask to see his/her status
|
|
if((subscription == 'from') || (subscription == 'none')) {
|
|
if(authorize_links)
|
|
authorize_links += ' / ';
|
|
|
|
authorize_links += '<a href="#" class="from">' + _e("Ask for authorization") + '</a>';
|
|
}
|
|
|
|
// Link to unblock this buddy
|
|
if((privacy_state == 'deny') && privacy_active) {
|
|
if(authorize_links)
|
|
authorize_links += ' / ';
|
|
|
|
html += '<a href="#" class="unblock">' + _e("Unblock") + '</a>';
|
|
}
|
|
|
|
html += authorize_links + '</p>';
|
|
}
|
|
|
|
// Complete the HTML code
|
|
var remove_links = '';
|
|
html += '<p class="bm-remove talk-images">';
|
|
remove_links = '<a href="#" class="remove">' + _e("Remove") + '</a>';
|
|
|
|
// This buddy is allowed to see our presence, we can show a "prohibit" link
|
|
if((subscription == 'both') || (subscription == 'from'))
|
|
remove_links += ' / <a href="#" class="prohibit">' + _e("Prohibit") + '</a>';
|
|
|
|
// Complete the HTML code
|
|
if((privacy_state != 'deny') && privacy_active) {
|
|
if(remove_links)
|
|
remove_links += ' / ';
|
|
|
|
remove_links += '<a href="#" class="block">' + _e("Block") + '</a>';
|
|
}
|
|
|
|
// Complete the HTML code
|
|
html += remove_links +
|
|
'</p>' +
|
|
'<p class="bm-rename talk-images"><label>' + _e("Rename") + '</label> <input type="text" value="' + encodeQuotes(nick) + '" /></p>';
|
|
|
|
// Only show group tool if not a gateway
|
|
if(!isGateway(xid))
|
|
html += '<p class="bm-group talk-images"><label>' + _e("Groups") + '</label> <input type="text" /></p>' +
|
|
'<div class="bm-choose">' +
|
|
'<div></div>' +
|
|
'</div>';
|
|
|
|
// Close the DOM element
|
|
html += '<a href="#" class="save">' + _e("Save") + '</a>' +
|
|
'</div>';
|
|
|
|
// We update the DOM elements
|
|
$(path + 'pep-infos').replaceWith(html);
|
|
|
|
// Gets all the existing groups
|
|
var all_groups = getAllGroups();
|
|
var all_groups_dom = '';
|
|
|
|
for(a in all_groups) {
|
|
// Current group
|
|
var all_groups_current = all_groups[a];
|
|
|
|
// Is the current group checked?
|
|
var checked = '';
|
|
|
|
if(existArrayValue(groups, all_groups_current))
|
|
checked = ' checked="true"';
|
|
|
|
// Add the current group HTML
|
|
all_groups_dom += '<label><input type="checkbox" data-group="' + escape(all_groups_current) + '"' + checked + ' /><span>' + all_groups_current.htmlEnc() + '</span></label>';
|
|
}
|
|
|
|
// Prepend this in the DOM
|
|
var bm_choose = path + 'manage-infos div.bm-choose';
|
|
|
|
$(bm_choose).prepend(all_groups_dom);
|
|
|
|
// Apply the editing input events
|
|
applyBuddyInput(xid);
|
|
}
|
|
|
|
// Unregisters from a given gateway
|
|
function unregisterGatewayRoster(xid) {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
iq.setTo(xid);
|
|
|
|
var query = iq.setQuery(NS_REGISTER);
|
|
query.appendChild(iq.buildNode('remove', {'xmlns': NS_REGISTER}));
|
|
|
|
con.send(iq);
|
|
}
|
|
|
|
// Updates the roster items
|
|
function sendRoster(xid, subscription, name, group) {
|
|
// We send the new buddy name
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var iqQuery = iq.setQuery(NS_ROSTER);
|
|
var item = iqQuery.appendChild(iq.buildNode('item', {'xmlns': NS_ROSTER, 'jid': xid}));
|
|
|
|
// Any subscription?
|
|
if(subscription)
|
|
item.setAttribute('subscription', subscription);
|
|
|
|
// Any name?
|
|
if(name)
|
|
item.setAttribute('name', name);
|
|
|
|
// Any group?
|
|
if(group && group.length) {
|
|
for(i in group)
|
|
item.appendChild(iq.buildNode('group', {'xmlns': NS_ROSTER}, group[i]));
|
|
}
|
|
|
|
con.send(iq);
|
|
|
|
logThis('Roster item sent: ' + xid, 3);
|
|
}
|
|
|
|
// Adapts the roster height, depending of the window size
|
|
function adaptRoster() {
|
|
// Process the new height
|
|
var new_height = $('#left-content').height() - $('#my-infos').height() - 97;
|
|
|
|
// New height too small
|
|
if(new_height < 211)
|
|
new_height = 211;
|
|
|
|
// Apply the new height
|
|
$('#buddy-list .content').css('height', new_height);
|
|
}
|
|
|
|
// Gets all the buddies in our roster
|
|
function getAllBuddies() {
|
|
var buddies = new Array();
|
|
|
|
$('#buddy-list .buddy').each(function() {
|
|
var xid = unescape($(this).attr('data-xid'));
|
|
|
|
if(xid)
|
|
buddies.push(xid);
|
|
});
|
|
|
|
return buddies;
|
|
}
|
|
|
|
// Gets the user gateways
|
|
function getGateways() {
|
|
// New array
|
|
var gateways = new Array();
|
|
var buddies = getAllBuddies();
|
|
|
|
// Get the gateways
|
|
for(c in buddies) {
|
|
if(isGateway(buddies[c]))
|
|
gateways.push(buddies[c]);
|
|
}
|
|
|
|
return gateways;
|
|
}
|
|
|
|
// Define a global var for buddy list all buddies displayed
|
|
var BLIST_ALL = false;
|
|
|
|
// Plugin launcher
|
|
function launchRoster() {
|
|
// Filtering tool
|
|
var iFilter = $('#buddy-list .filter input');
|
|
var aFilter = $('#buddy-list .filter a');
|
|
|
|
iFilter.placeholder()
|
|
|
|
.blur(function() {
|
|
// Nothing is entered, put the placeholder instead
|
|
if(!trim($(this).val()))
|
|
aFilter.hide();
|
|
else
|
|
aFilter.show();
|
|
})
|
|
|
|
.keyup(function(e) {
|
|
funnelFilterBuddySearch(e.keyCode);
|
|
});
|
|
|
|
aFilter.click(function() {
|
|
// Reset the input
|
|
$(this).hide();
|
|
iFilter.val('');
|
|
iFilter.placeholder();
|
|
|
|
// Security: show all the groups, empty or not
|
|
$('#buddy-list .one-group').show();
|
|
|
|
// Reset the filtering tool
|
|
resetFilterBuddySearch();
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user click on the add button, show the contact adding tool
|
|
$('#buddy-list .foot .add').click(function() {
|
|
// Yet displayed?
|
|
if(exists('#buddy-conf-add'))
|
|
return closeBubbles();
|
|
|
|
// Add the bubble
|
|
showBubble('#buddy-conf-add');
|
|
|
|
// Append the content
|
|
$('#buddy-list .buddy-list-add').append(
|
|
'<div id="buddy-conf-add" class="buddy-conf-item bubble removable">' +
|
|
'<div class="buddy-conf-subarrow talk-images"></div>' +
|
|
|
|
'<div class="buddy-conf-subitem">' +
|
|
'<p class="buddy-conf-p">' + _e("Add a friend") + '</p>' +
|
|
|
|
'<label><span>' + _e("Address") + '</span><input type="text" class="buddy-conf-input add-contact-jid" required="" /></label>' +
|
|
'<label><span>' + _e("Name") + '</span><input type="text" class="buddy-conf-input add-contact-name" /></label>' +
|
|
'<label>' +
|
|
'<span>' + _e("Gateway") + '</span>' +
|
|
'<select class="buddy-conf-select add-contact-gateway">' +
|
|
'<option value="none" selected="">' + _e("None") + '</option>' +
|
|
'</select>' +
|
|
'</label>' +
|
|
'<span class="add-contact-name-get">' + _e("Getting the name...") + '</span>' +
|
|
|
|
'<p class="buddy-conf-text">' +
|
|
'<a href="#" class="buddy-conf-add-search">' + _e("Search a friend") + '</a>' +
|
|
'</p>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Add the gateways
|
|
var gateways = getGateways();
|
|
|
|
// Any gateway?
|
|
if(gateways.length) {
|
|
// Append the gateways
|
|
for(i in gateways)
|
|
$('.add-contact-gateway').append('<option value="' + escape(gateways[i]) + '">' + gateways[i].htmlEnc() + '</option>');
|
|
|
|
// Show the gateway selector
|
|
$('.add-contact-gateway').parent().show();
|
|
}
|
|
|
|
// No gateway?
|
|
else
|
|
$('.add-contact-gateway').parent().hide();
|
|
|
|
// Blur event on the add contact input
|
|
$('.add-contact-jid').blur(function() {
|
|
// Read the value
|
|
var value = trim($(this).val());
|
|
|
|
// Try to catch the buddy name
|
|
if(value && !trim($('.add-contact-name').val()) && ($('.add-contact-gateway').val() == 'none')) {
|
|
// User XID
|
|
var xid = generateXID(value, 'chat');
|
|
|
|
// Notice for the user
|
|
$('.add-contact-name-get').attr('data-for', escape(xid)).show();
|
|
|
|
// Request the user vCard
|
|
getAddUserName(xid);
|
|
}
|
|
});
|
|
|
|
// When a key is pressed...
|
|
$('#buddy-conf-add input, #buddy-conf-add select').keyup(function(e) {
|
|
// Enter : continue
|
|
if(e.keyCode == 13) {
|
|
// Get the values
|
|
var xid = trim($('.add-contact-jid').val());
|
|
var name = trim($('.add-contact-name').val());
|
|
var gateway = unescape($('.add-contact-gateway').val());
|
|
|
|
// Generate the XID to add
|
|
if((gateway != 'none') && xid)
|
|
xid = xid.replace(/@/g, '%') + '@' + gateway;
|
|
else
|
|
xid = generateXID(xid, 'chat');
|
|
|
|
// Submit the form
|
|
if(xid && getXIDNick(xid) && (xid != getXID()))
|
|
addThisContact(xid, name);
|
|
else
|
|
$(document).oneTime(10, function() {
|
|
$('.add-contact-jid').addClass('please-complete').focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Escape : quit
|
|
if(e.keyCode == 27)
|
|
closeBubbles();
|
|
});
|
|
|
|
// Click event on search link
|
|
$('.buddy-conf-add-search').click(function() {
|
|
closeBubbles();
|
|
return openDirectory();
|
|
});
|
|
|
|
// Focus on the input
|
|
$(document).oneTime(10, function() {
|
|
$('.add-contact-jid').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user click on the join button, show the chat joining tool
|
|
$('#buddy-list .foot .join').click(function() {
|
|
// Yet displayed?
|
|
if(exists('#buddy-conf-join'))
|
|
return closeBubbles();
|
|
|
|
// Add the bubble
|
|
showBubble('#buddy-conf-join');
|
|
|
|
// Append the content
|
|
$('#buddy-list .buddy-list-join').append(
|
|
'<div id="buddy-conf-join" class="buddy-conf-item bubble removable">' +
|
|
'<div class="buddy-conf-subarrow talk-images"></div>' +
|
|
|
|
'<div class="buddy-conf-subitem search">' +
|
|
'<p class="buddy-conf-p" style="margin-bottom: 0;">' + _e("Join a chat") + '</p>' +
|
|
|
|
'<input type="text" class="buddy-conf-input join-jid" required="" />' +
|
|
'<select class="buddy-conf-select buddy-conf-join-select join-type">' +
|
|
'<option value="chat" selected="">' + _e("Chat") + '</option>' +
|
|
'<option value="groupchat">' + _e("Groupchat") + '</option>' +
|
|
'</select>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Input vars
|
|
var destination = '#buddy-conf-join .search';
|
|
var dHovered = destination + ' ul li.hovered:first';
|
|
|
|
// When a key is pressed...
|
|
$('#buddy-conf-join input, #buddy-conf-join select').keyup(function(e) {
|
|
// Enter: continue
|
|
if(e.keyCode == 13) {
|
|
// Select something from the search
|
|
if(exists(dHovered))
|
|
addBuddySearch(destination, $(dHovered).attr('data-xid'));
|
|
|
|
// Join something
|
|
else {
|
|
var xid = trim($('.join-jid').val());
|
|
var type = $('.buddy-conf-join-select').val();
|
|
|
|
if(xid && type) {
|
|
// Generate a correct XID
|
|
xid = generateXID(xid, type);
|
|
|
|
// Not me
|
|
if(xid != getXID()) {
|
|
// Update some things
|
|
$('.join-jid').removeClass('please-complete');
|
|
closeBubbles();
|
|
|
|
// Create a new chat
|
|
checkChatCreate(xid, type);
|
|
}
|
|
|
|
else
|
|
$('.join-jid').addClass('please-complete');
|
|
}
|
|
|
|
else
|
|
$('.join-jid').addClass('please-complete');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Escape: quit
|
|
else if(e.keyCode == 27)
|
|
closeBubbles();
|
|
|
|
// Buddy search?
|
|
else if($('.buddy-conf-join-select').val() == 'chat') {
|
|
// New buddy search
|
|
if((e.keyCode != 40) && (e.keyCode != 38))
|
|
createBuddySearch(destination);
|
|
|
|
// Navigating with keyboard in the results
|
|
arrowsBuddySearch(e, destination);
|
|
}
|
|
});
|
|
|
|
// Buddy search lost focus
|
|
$('#buddy-conf-join input').blur(function() {
|
|
if(!$(destination + ' ul').attr('mouse-hover'))
|
|
resetBuddySearch(destination);
|
|
});
|
|
|
|
// Re-focus on the text input
|
|
$('#buddy-conf-join select').change(function() {
|
|
$(document).oneTime(10, function() {
|
|
$('#buddy-conf-join input').focus();
|
|
});
|
|
});
|
|
|
|
// We focus on the input
|
|
$(document).oneTime(10, function() {
|
|
$('#buddy-conf-join .join-jid').focus();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user click on the groupchat button, show the groupchat menu
|
|
$('#buddy-list .foot .groupchat').click(function() {
|
|
// Yet displayed?
|
|
if(exists('#buddy-conf-groupchat'))
|
|
return closeBubbles();
|
|
|
|
// Add the bubble
|
|
showBubble('#buddy-conf-groupchat');
|
|
|
|
// Append the content
|
|
$('#buddy-list .buddy-list-groupchat').append(
|
|
'<div id="buddy-conf-groupchat" class="buddy-conf-item bubble removable">' +
|
|
'<div class="buddy-conf-subarrow talk-images"></div>' +
|
|
|
|
'<div class="buddy-conf-subitem">' +
|
|
'<p class="buddy-conf-p">' + _e("Your groupchats") + '</p>' +
|
|
|
|
'<select name="groupchat-join" class="buddy-conf-select buddy-conf-groupchat-select"></select>' +
|
|
|
|
'<p class="buddy-conf-text">' +
|
|
'- <a href="#" class="buddy-conf-groupchat-edit">' + _e("Manage your favorite groupchats") + '</a>' +
|
|
'</p>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// When the user wants to edit his groupchat favorites
|
|
$('.buddy-conf-groupchat-edit').click(function() {
|
|
openFavorites();
|
|
closeBubbles();
|
|
|
|
return false;
|
|
});
|
|
|
|
// Change event
|
|
$('.buddy-conf-groupchat-select').change(function() {
|
|
var groupchat = trim($(this).val());
|
|
|
|
if(groupchat != 'none') {
|
|
// We hide the bubble
|
|
closeBubbles();
|
|
|
|
// Create the chat
|
|
checkChatCreate(groupchat, 'groupchat');
|
|
|
|
// We reset the select value
|
|
$(this).val('none');
|
|
}
|
|
});
|
|
|
|
// Load the favorites
|
|
loadFavorites();
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user click on the more button, show the more menu
|
|
$('#buddy-list .foot .more').click(function() {
|
|
// Yet displayed?
|
|
if(exists('#buddy-conf-more'))
|
|
return closeBubbles();
|
|
|
|
// Add the bubble
|
|
showBubble('#buddy-conf-more');
|
|
|
|
// Append the content
|
|
$('#buddy-list .buddy-list-more').append(
|
|
'<div id="buddy-conf-more" class="buddy-conf-item bubble removable">' +
|
|
'<div class="buddy-conf-subarrow talk-images"></div>' +
|
|
|
|
'<div class="buddy-conf-subitem">' +
|
|
'<p class="buddy-conf-p">' + _e("More stuff") + '</p>' +
|
|
|
|
'<p class="buddy-conf-text">' +
|
|
'- <a href="#" class="buddy-conf-more-display-unavailable">' + _e("Show all friends") + '</a>' +
|
|
'<a href="#" class="buddy-conf-more-display-available">' + _e("Only show connected friends") + '</a>' +
|
|
'</p>' +
|
|
|
|
'<p class="buddy-conf-text privacy-hidable">' +
|
|
'- <a href="#" class="buddy-conf-more-privacy">' + _e("Privacy") + '</a>' +
|
|
'</p>' +
|
|
|
|
'<p class="buddy-conf-text">' +
|
|
'- <a href="#" class="buddy-conf-more-service-disco">' + _e("Service discovery") + '</a>' +
|
|
'</p>' +
|
|
|
|
'<p class="buddy-conf-text commands-hidable"">' +
|
|
'- <a href="#" class="buddy-conf-more-commands">' + _e("Commands") + '</a>' +
|
|
'</p>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
|
|
// Close bubble when link clicked
|
|
$('#buddy-conf-more a').click(function() {
|
|
closeBubbles();
|
|
});
|
|
|
|
// When the user wants to display all his buddies
|
|
$('.buddy-conf-more-display-unavailable').click(function() {
|
|
showAllBuddies('roster');
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user wants to display only online buddies
|
|
$('.buddy-conf-more-display-available').click(function() {
|
|
showOnlineBuddies('roster');
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user click on the privacy link
|
|
$('.buddy-conf-more-privacy').click(openPrivacy);
|
|
|
|
// When the user click on the service discovery link
|
|
$('.buddy-conf-more-service-disco').click(openDiscovery);
|
|
|
|
// When the user click on the command link
|
|
$('.buddy-conf-more-commands').click(function() {
|
|
serverAdHoc(con.domain);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Manage the displayed links
|
|
if(BLIST_ALL) {
|
|
$('.buddy-conf-more-display-unavailable').hide();
|
|
$('.buddy-conf-more-display-available').show();
|
|
}
|
|
|
|
if(enabledCommands())
|
|
$('.buddy-conf-more-commands').parent().show();
|
|
|
|
if(getDB('privacy-marker', 'available'))
|
|
$('.buddy-conf-more-privacy').parent().show();
|
|
|
|
return false;
|
|
});
|
|
|
|
// When the user scrolls the buddy list
|
|
$('#buddy-list .content').scroll(function() {
|
|
// Close the opened buddy infos bubble
|
|
closeBubbles();
|
|
});
|
|
}
|
|
|
|
// Window resize event handler
|
|
$(window).resize(adaptRoster);
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the storage JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 14/05/13
|
|
|
|
*/
|
|
|
|
// Gets the storage items of the user
|
|
function getStorage(type) {
|
|
/* REF: http://xmpp.org/extensions/xep-0049.html */
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
var iqQuery = iq.setQuery(NS_PRIVATE);
|
|
iqQuery.appendChild(iq.buildNode('storage', {'xmlns': type}));
|
|
|
|
con.send(iq, handleStorage);
|
|
}
|
|
|
|
// Handles the storage items
|
|
function handleStorage(iq) {
|
|
var handleXML = iq.getQuery();
|
|
var handleFrom = fullXID(getStanzaFrom(iq));
|
|
|
|
// Define some vars
|
|
var options = $(handleXML).find('storage[xmlns="' + NS_OPTIONS + '"]');
|
|
var inbox = $(handleXML).find('storage[xmlns="' + NS_INBOX + '"]');
|
|
var bookmarks = $(handleXML).find('storage[xmlns="' + NS_BOOKMARKS + '"]');
|
|
var rosternotes = $(handleXML).find('storage[xmlns="' + NS_ROSTERNOTES + '"]');
|
|
|
|
// No options and node not yet configured
|
|
if(options.size() && !options.find('option').size() && (iq.getType() != 'error'))
|
|
openWelcome();
|
|
|
|
// Parse the options xml
|
|
options.find('option').each(function() {
|
|
// We retrieve the informations
|
|
var type = $(this).attr('type');
|
|
var value = $(this).text();
|
|
|
|
// We display the storage
|
|
setDB('options', type, value);
|
|
|
|
// If this is the buddy list show status
|
|
if((type == 'roster-showall') && (value == '1'))
|
|
showAllBuddies('storage');
|
|
});
|
|
|
|
// Parse the inbox xml
|
|
inbox.find('message').each(function() {
|
|
storeInboxMessage(
|
|
$(this).attr('from'),
|
|
$(this).attr('subject'),
|
|
$(this).text(),
|
|
$(this).attr('status'),
|
|
$(this).attr('id'),
|
|
$(this).attr('date'),
|
|
[
|
|
$(this).attr('file_title'),
|
|
$(this).attr('file_href'),
|
|
$(this).attr('file_type'),
|
|
$(this).attr('file_length')
|
|
]
|
|
);
|
|
});
|
|
|
|
// Parse the bookmarks xml
|
|
bookmarks.find('conference').each(function() {
|
|
// We retrieve the informations
|
|
var xid = $(this).attr('jid');
|
|
var name = $(this).attr('name');
|
|
var autojoin = $(this).attr('autojoin');
|
|
var password = $(this).find('password').text();
|
|
var nick = $(this).find('nick').text();
|
|
|
|
// Filter autojoin (compatibility)
|
|
autojoin = ((autojoin == 'true') || (autojoin == '1')) ? 'true' : 'false';
|
|
|
|
// We display the storage
|
|
displayFavorites(xid, name, nick, autojoin, password);
|
|
|
|
// Join the chat if autojoin is enabled
|
|
if(autojoin == 'true')
|
|
checkChatCreate(xid, 'groupchat', nick, password, name);
|
|
});
|
|
|
|
// Parse the roster notes xml
|
|
rosternotes.find('note').each(function() {
|
|
setDB('rosternotes', $(this).attr('jid'), $(this).text());
|
|
});
|
|
|
|
// Options received
|
|
if(options.size()) {
|
|
logThis('Options received.');
|
|
|
|
// Now, get the inbox
|
|
getStorage(NS_INBOX);
|
|
|
|
// Geolocate the user
|
|
geolocate();
|
|
|
|
$('.options-hidable').show();
|
|
}
|
|
|
|
// Inbox received
|
|
else if(inbox.size()) {
|
|
logThis('Inbox received.');
|
|
|
|
// Send the first presence!
|
|
firstPresence(getDB('checksum', 1));
|
|
|
|
// Check we have new messages (play a sound if any unread messages)
|
|
if(checkInboxMessages())
|
|
soundPlay(2);
|
|
|
|
$('.inbox-hidable').show();
|
|
}
|
|
|
|
// Bookmarks received
|
|
else if(bookmarks.size()) {
|
|
// Join the groupchats the admin defined (if any)
|
|
joinConfGroupchats();
|
|
|
|
logThis('Bookmarks received.');
|
|
}
|
|
|
|
// Roster notes received (for logger)
|
|
else if(rosternotes.size())
|
|
logThis('Roster notes received.');
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the common JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: dual-licensed under AGPL and MPLv2
|
|
Authors: Valérian Saliou, olivierm, regilero, Maranda
|
|
Last revision: 24/09/12
|
|
|
|
*/
|
|
|
|
// Checks if an element exists in the DOM
|
|
function exists(selector) {
|
|
if(jQuery(selector).size() > 0)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// Checks if Jappix is connected
|
|
function isConnected() {
|
|
if((typeof con != 'undefined') && con && con.connected())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if Jappix has focus
|
|
function isFocused() {
|
|
try {
|
|
if(document.hasFocus())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
catch(e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Generates the good XID
|
|
function generateXID(xid, type) {
|
|
// XID needs to be transformed
|
|
// .. and made lowercase (uncertain though this is the right place...)
|
|
xid = xid.toLowerCase();
|
|
|
|
if(xid && (xid.indexOf('@') == -1)) {
|
|
// Groupchat
|
|
if(type == 'groupchat')
|
|
return xid + '@' + HOST_MUC;
|
|
|
|
// One-to-one chat
|
|
if(xid.indexOf('.') == -1)
|
|
return xid + '@' + HOST_MAIN;
|
|
|
|
// It might be a gateway?
|
|
return xid;
|
|
}
|
|
|
|
// Nothing special (yet bare XID)
|
|
return xid;
|
|
}
|
|
|
|
// Gets the asked translated string
|
|
function _e(string) {
|
|
return string;
|
|
}
|
|
|
|
// Replaces '%s' to a given value for a translated string
|
|
function printf(string, value) {
|
|
return string.replace('%s', value);
|
|
}
|
|
|
|
// Returns the string after the last given char
|
|
function strAfterLast(given_char, str) {
|
|
if(!given_char || !str)
|
|
return '';
|
|
|
|
var char_index = str.lastIndexOf(given_char);
|
|
var str_return = str;
|
|
|
|
if(char_index >= 0)
|
|
str_return = str.substr(char_index + 1);
|
|
|
|
return str_return;
|
|
}
|
|
|
|
// Properly explodes a string with a given character
|
|
function explodeThis(toEx, toStr, i) {
|
|
// Get the index of our char to explode
|
|
var index = toStr.indexOf(toEx);
|
|
|
|
// We split if necessary the string
|
|
if(index != -1) {
|
|
if(i == 0)
|
|
toStr = toStr.substr(0, index);
|
|
else
|
|
toStr = toStr.substr(index + 1);
|
|
}
|
|
|
|
// We return the value
|
|
return toStr;
|
|
}
|
|
|
|
// Cuts the resource of a XID
|
|
function cutResource(aXID) {
|
|
return explodeThis('/', aXID, 0);
|
|
}
|
|
|
|
// Gets the resource of a XID
|
|
function thisResource(aXID) {
|
|
// Any resource?
|
|
if(aXID.indexOf('/') != -1)
|
|
return explodeThis('/', aXID, 1);
|
|
|
|
// No resource
|
|
return '';
|
|
}
|
|
|
|
// nodepreps an XMPP node
|
|
function nodeprep(node) {
|
|
// Spec: http://tools.ietf.org/html/rfc6122#appendix-A
|
|
|
|
if(!node)
|
|
return node;
|
|
|
|
// Remove prohibited chars
|
|
var prohibited_chars = ['"', '&', '\'', '/', ':', '<', '>', '@'];
|
|
|
|
for(j in prohibited_chars)
|
|
node = node.replace(prohibited_chars[j], '');
|
|
|
|
// Lower case
|
|
node = node.toLowerCase();
|
|
|
|
return node;
|
|
}
|
|
|
|
// Encodes quotes in a string
|
|
function encodeQuotes(str) {
|
|
return (str + '').htmlEnc();
|
|
}
|
|
|
|
// Gets the bare XID from a XID
|
|
function bareXID(xid) {
|
|
// Cut the resource
|
|
xid = cutResource(xid);
|
|
|
|
// Launch nodeprep
|
|
if(xid.indexOf('@') != -1)
|
|
xid = nodeprep(getXIDNick(xid)) + '@' + getXIDHost(xid);
|
|
|
|
return xid;
|
|
}
|
|
|
|
// Gets the full XID from a XID
|
|
function fullXID(xid) {
|
|
// Normalizes the XID
|
|
var full = bareXID(xid);
|
|
var resource = thisResource(xid);
|
|
|
|
// Any resource?
|
|
if(resource)
|
|
full += '/' + resource;
|
|
|
|
return full;
|
|
}
|
|
|
|
// Gets the nick from a XID
|
|
function getXIDNick(aXID) {
|
|
// Gateway nick?
|
|
if(aXID.match(/\\40/))
|
|
return explodeThis('\\40', aXID, 0);
|
|
|
|
return explodeThis('@', aXID, 0);
|
|
}
|
|
|
|
// Gets the host from a XID
|
|
function getXIDHost(aXID) {
|
|
return explodeThis('@', aXID, 1);
|
|
}
|
|
|
|
// Checks if we are in developer mode
|
|
function isDeveloper() {
|
|
return (DEVELOPER == 'on');
|
|
}
|
|
|
|
// Checks if we are RTL (Right-To-Left)
|
|
function isRTL() {
|
|
return (_e("default:LTR") == 'default:RTL');
|
|
}
|
|
|
|
// Checks if anonymous mode is allowed
|
|
function allowedAnonymous() {
|
|
return (ANONYMOUS == 'on');
|
|
}
|
|
|
|
// Checks if host is locked
|
|
function lockHost() {
|
|
return (LOCK_HOST == 'on');
|
|
}
|
|
|
|
// Gets the full XID of the user
|
|
function getXID() {
|
|
// Return the XID of the user
|
|
if(con.username && con.domain)
|
|
return con.username + '@' + con.domain;
|
|
|
|
return '';
|
|
}
|
|
|
|
// Generates the colors for a given user XID
|
|
function generateColor(xid) {
|
|
var colors = new Array(
|
|
'ac0000',
|
|
'a66200',
|
|
'007703',
|
|
'00705f',
|
|
'00236b',
|
|
'4e005c'
|
|
);
|
|
|
|
var number = 0;
|
|
|
|
for(var i = 0; i < xid.length; i++)
|
|
number += xid.charCodeAt(i);
|
|
|
|
var color = '#' + colors[number % (colors.length)];
|
|
|
|
return color;
|
|
}
|
|
|
|
// Checks if the XID is a gateway
|
|
function isGateway(xid) {
|
|
if(xid.indexOf('@') != -1)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Gets the from attribute of a stanza (overrides some servers like Prosody missing from attributes)
|
|
function getStanzaFrom(stanza) {
|
|
var from = stanza.getFrom();
|
|
|
|
// No from, we assume this is our XID
|
|
if(!from)
|
|
from = getXID();
|
|
|
|
return from;
|
|
}
|
|
|
|
// Logs a given data in the console
|
|
function logThis(data, level) {
|
|
// Console not available
|
|
if(!isDeveloper() || (typeof(console) == 'undefined'))
|
|
return false;
|
|
|
|
// Switch the log level
|
|
switch(level) {
|
|
// Debug
|
|
case 0:
|
|
console.debug(data);
|
|
|
|
break;
|
|
|
|
// Error
|
|
case 1:
|
|
console.error(data);
|
|
|
|
break;
|
|
|
|
// Warning
|
|
case 2:
|
|
console.warn(data);
|
|
|
|
break;
|
|
|
|
// Information
|
|
case 3:
|
|
console.info(data);
|
|
|
|
break;
|
|
|
|
// Default log level
|
|
default:
|
|
console.log(data);
|
|
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Gets the current Jappix app. location
|
|
function getJappixLocation() {
|
|
var url = window.location.href;
|
|
|
|
// If the URL has variables, remove them
|
|
if(url.indexOf('?') != -1)
|
|
url = url.split('?')[0];
|
|
if(url.indexOf('#') != -1)
|
|
url = url.split('#')[0];
|
|
|
|
// No "/" at the end
|
|
if(!url.match(/(.+)\/$/))
|
|
url += '/';
|
|
|
|
return url;
|
|
}
|
|
|
|
// Removes spaces at the beginning & the end of a string
|
|
function trim(str) {
|
|
return str.replace(/^\s+/g,'').replace(/\s+$/g,'');
|
|
}
|
|
|
|
// Adds a zero to a date when needed
|
|
function padZero(i) {
|
|
// Negative number (without first 0)
|
|
if(i > -10 && i < 0)
|
|
return '-0' + (i * -1);
|
|
|
|
// Positive number (without first 0)
|
|
if(i < 10 && i >= 0)
|
|
return '0' + i;
|
|
|
|
// All is okay
|
|
return i;
|
|
}
|
|
|
|
// Escapes a string for a regex usage
|
|
function escapeRegex(query) {
|
|
return query.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
}
|
|
|
|
// Returns a random array value
|
|
function randomArrayValue(arr) {
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
}
|
|
|
|
// Returns whether the browser is mobile or not
|
|
function isMobile() {
|
|
try {
|
|
return /Android|iPhone|iPod|iPad|Windows Phone|BlackBerry|Bada|Maemo|Meego|webOS/i.test(navigator.userAgent);
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the utilities JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Authors: Valérian Saliou, olivierm
|
|
Last revision: 24/06/11
|
|
|
|
*/
|
|
|
|
// Checks if a function exists
|
|
function functionExists(func) {
|
|
if(typeof func == 'function')
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Returns whether using HTTPS or not
|
|
function isHTTPS() {
|
|
if(window.location.href && (window.location.href).match(/^https/i))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Generates the good storage URL
|
|
function generateURL(url) {
|
|
// HTTPS not allowed
|
|
if((HTTPS_STORAGE != 'on') && url.match(/^https(.+)/))
|
|
url = 'http' + RegExp.$1;
|
|
|
|
return url;
|
|
}
|
|
|
|
// Disables an input if needed
|
|
function disableInput(value, condition) {
|
|
if(value == condition)
|
|
return ' disabled=""';
|
|
|
|
return '';
|
|
}
|
|
|
|
// Cuts a string
|
|
function cut(string, limit) {
|
|
return string.substr(0, limit);
|
|
}
|
|
|
|
// Truncates a string
|
|
function truncate(string, limit) {
|
|
// Must truncate the string
|
|
if(string.length > limit)
|
|
string = string.substr(0, limit) + '...';
|
|
|
|
return string;
|
|
}
|
|
|
|
// Removes the new lines
|
|
function noLines(string) {
|
|
return string.replace(/\n/g, ' ');
|
|
}
|
|
|
|
// Encodes a string for onclick attribute
|
|
function encodeOnclick(str) {
|
|
return (encodeQuotes(str)).replace(/'/g, '\\$&');
|
|
}
|
|
|
|
// Checks whether the passed parameter is a number or not
|
|
function isNumber(n) {
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
}
|
|
|
|
// Checks if we are in the anonymous mode
|
|
function isAnonymous() {
|
|
if(allowedAnonymous() && LINK_VARS['r'])
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if this is a private chat user
|
|
function isPrivate(xid) {
|
|
if(exists('[data-xid="' + escape(xid) + '"][data-type="groupchat"]'))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Checks if the user browser is obsolete
|
|
function isObsolete() {
|
|
// Get browser name & version
|
|
var browser_name = BrowserDetect.browser;
|
|
var browser_version = BrowserDetect.version;
|
|
|
|
// No DOM storage
|
|
if(!hasDB() || !hasPersistent())
|
|
return true;
|
|
|
|
// Obsolete IE
|
|
if((browser_name == 'Explorer') && (browser_version < 8))
|
|
return true;
|
|
|
|
// Obsolete Chrome
|
|
if((browser_name == 'Chrome') && (browser_version < 7))
|
|
return true;
|
|
|
|
// Obsolete Safari
|
|
if((browser_name == 'Safari') && (browser_version < 4))
|
|
return true;
|
|
|
|
// Obsolete Firefox
|
|
if((browser_name == 'Firefox') && (browser_version < 3.5))
|
|
return true;
|
|
|
|
// Obsolete Opera
|
|
if((browser_name == 'Opera') && (browser_version < 9))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets a MUC user XID
|
|
function getMUCUserXID(room, nick) {
|
|
return $('div.chat[data-xid="' + escape(room) + '"] div[data-nick="' + escape(nick) + '"]').attr('data-xid');
|
|
}
|
|
|
|
// Gets a MUC user read XID
|
|
function getMUCUserRealXID(room, nick) {
|
|
return $('div.chat[data-xid="' + escape(room) + '"] div[data-nick="' + escape(nick) + '"]').attr('data-realxid');
|
|
}
|
|
|
|
// Gets the server of the user
|
|
function getServer() {
|
|
// Return the domain of the user
|
|
return con.domain;
|
|
}
|
|
|
|
// Gets the password of the user
|
|
function getPassword() {
|
|
// Return the password of the user
|
|
return con.pass;
|
|
}
|
|
|
|
// Quotes the nick of an user
|
|
function quoteMyNick(hash, nick) {
|
|
$(document).oneTime(10, function() {
|
|
$('#page-engine #' + hash + ' .message-area').val(nick + ', ').focus();
|
|
});
|
|
}
|
|
|
|
// Converts a XML document to a string
|
|
function xmlToString(xmlData) {
|
|
try {
|
|
// For Mozilla, Firefox, Opera, etc.
|
|
if(window.XMLSerializer)
|
|
return (new XMLSerializer()).serializeToString(xmlData);
|
|
|
|
// For Internet Explorer
|
|
if(window.ActiveXObject)
|
|
return xmlData.xml;
|
|
|
|
return null;
|
|
}
|
|
|
|
catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Converts a string to a XML document
|
|
function XMLFromString(sXML) {
|
|
try {
|
|
// No data?
|
|
if(!sXML)
|
|
return '';
|
|
|
|
// Add the XML tag
|
|
if(!sXML.match(/^<\?xml/i))
|
|
sXML = '<?xml version="1.0"?>' + sXML;
|
|
|
|
// Parse it!
|
|
if(window.DOMParser)
|
|
return (new DOMParser()).parseFromString(sXML, 'text/xml');
|
|
|
|
if(window.ActiveXObject) {
|
|
var oXML = new ActiveXObject('Microsoft.XMLDOM');
|
|
oXML.loadXML(sXML);
|
|
|
|
return oXML;
|
|
}
|
|
}
|
|
|
|
catch(e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Return the file category
|
|
function fileCategory(ext) {
|
|
var cat;
|
|
|
|
switch(ext) {
|
|
// Images
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
case 'png':
|
|
case 'bmp':
|
|
case 'gif':
|
|
case 'tif':
|
|
case 'svg':
|
|
case 'ico':
|
|
case 'psp':
|
|
case 'psd':
|
|
case 'psb':
|
|
case 'xcf':
|
|
cat = 'image';
|
|
|
|
break;
|
|
|
|
// Videos
|
|
case 'ogv':
|
|
case 'ogg':
|
|
case 'mkv':
|
|
case 'avi':
|
|
case 'mov':
|
|
case 'mp4':
|
|
case 'm4v':
|
|
case 'wmv':
|
|
case 'asf':
|
|
case 'mpg':
|
|
case 'mpeg':
|
|
case 'ogm':
|
|
case 'rmvb':
|
|
case 'rmv':
|
|
case 'qt':
|
|
case 'flv':
|
|
case 'ram':
|
|
case '3gp':
|
|
case 'avc':
|
|
cat = 'video';
|
|
|
|
break;
|
|
|
|
// Sounds
|
|
case 'oga':
|
|
case 'mka':
|
|
case 'flac':
|
|
case 'mp3':
|
|
case 'wav':
|
|
case 'm4a':
|
|
case 'wma':
|
|
case 'rmab':
|
|
case 'rma':
|
|
case 'bwf':
|
|
case 'aiff':
|
|
case 'caf':
|
|
case 'cda':
|
|
case 'atrac':
|
|
case 'vqf':
|
|
case 'au':
|
|
case 'aac':
|
|
case 'm3u':
|
|
case 'mid':
|
|
case 'mp2':
|
|
case 'snd':
|
|
case 'voc':
|
|
cat = 'audio';
|
|
|
|
break;
|
|
|
|
// Documents
|
|
case 'pdf':
|
|
case 'odt':
|
|
case 'ott':
|
|
case 'sxw':
|
|
case 'stw':
|
|
case 'ots':
|
|
case 'sxc':
|
|
case 'stc':
|
|
case 'sxi':
|
|
case 'sti':
|
|
case 'pot':
|
|
case 'odp':
|
|
case 'ods':
|
|
case 'doc':
|
|
case 'docx':
|
|
case 'docm':
|
|
case 'xls':
|
|
case 'xlsx':
|
|
case 'xlsm':
|
|
case 'xlt':
|
|
case 'ppt':
|
|
case 'pptx':
|
|
case 'pptm':
|
|
case 'pps':
|
|
case 'odg':
|
|
case 'otp':
|
|
case 'sxd':
|
|
case 'std':
|
|
case 'std':
|
|
case 'rtf':
|
|
case 'txt':
|
|
case 'htm':
|
|
case 'html':
|
|
case 'shtml':
|
|
case 'dhtml':
|
|
case 'mshtml':
|
|
cat = 'document';
|
|
|
|
break;
|
|
|
|
// Packages
|
|
case 'tgz':
|
|
case 'gz':
|
|
case 'tar':
|
|
case 'ar':
|
|
case 'cbz':
|
|
case 'jar':
|
|
case 'tar.7z':
|
|
case 'tar.bz2':
|
|
case 'tar.gz':
|
|
case 'tar.lzma':
|
|
case 'tar.xz':
|
|
case 'zip':
|
|
case 'xz':
|
|
case 'rar':
|
|
case 'bz':
|
|
case 'deb':
|
|
case 'rpm':
|
|
case '7z':
|
|
case 'ace':
|
|
case 'cab':
|
|
case 'arj':
|
|
case 'msi':
|
|
cat = 'package';
|
|
|
|
break;
|
|
|
|
// Others
|
|
default:
|
|
cat = 'other';
|
|
|
|
break;
|
|
}
|
|
|
|
return cat;
|
|
}
|
|
|
|
// Registers Jappix as the default XMPP links handler
|
|
function xmppLinksHandler() {
|
|
try {
|
|
navigator.registerProtocolHandler('xmpp', JAPPIX_LOCATION + '?x=%s', SERVICE_NAME);
|
|
|
|
return true;
|
|
}
|
|
|
|
catch(e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Checks if a value exists in an array
|
|
function existArrayValue(array, value) {
|
|
try {
|
|
// Loop in the array
|
|
for(i in array) {
|
|
if(array[i] == value)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
catch(e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Removes a value from an array
|
|
function removeArrayValue(array, value) {
|
|
for(i in array) {
|
|
// It matches, remove it!
|
|
if(array[i] == value) {
|
|
array.splice(i, 1);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Converts a string to an array
|
|
function stringToArray(string) {
|
|
var array = [];
|
|
|
|
// Any string to convert?
|
|
if(string) {
|
|
// More than one item
|
|
if(string.match(/,/gi)) {
|
|
var string_split = string.split(',');
|
|
|
|
for(i in string_split) {
|
|
if(string_split[i])
|
|
array.push(string_split[i]);
|
|
else
|
|
array.push('');
|
|
}
|
|
}
|
|
|
|
// Only one item
|
|
else
|
|
array.push(string);
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
// Get the index of an array value
|
|
function indexArrayValue(array, value) {
|
|
// Nothing?
|
|
if(!array || !array.length)
|
|
return 0;
|
|
|
|
// Read the index of the value
|
|
var index = 0;
|
|
|
|
for(var i = 0; i < array.length; i++) {
|
|
if(array[i] == value) {
|
|
index = i;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
// Capitalizes the first letter of a string
|
|
function capitaliseFirstLetter(string) {
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the date related JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: dual-licensed under AGPL and MPLv2
|
|
Author: Valérian Saliou
|
|
Last revision: 17/08/11
|
|
|
|
*/
|
|
|
|
// Gets a stamp from a date
|
|
function extractStamp(date) {
|
|
return Math.round(date.getTime() / 1000);
|
|
}
|
|
|
|
// Gets the time from a date
|
|
function extractTime(date) {
|
|
return date.toLocaleTimeString();
|
|
}
|
|
|
|
// Gets the actual date stamp
|
|
function getTimeStamp() {
|
|
return extractStamp(new Date());
|
|
}
|
|
|
|
// Gets the last user activity in seconds
|
|
var LAST_ACTIVITY = 0;
|
|
|
|
function getLastActivity() {
|
|
// Last activity not yet initialized?
|
|
if(LAST_ACTIVITY == 0)
|
|
return 0;
|
|
|
|
return getTimeStamp() - LAST_ACTIVITY;
|
|
}
|
|
|
|
// Gets the last user available presence in seconds
|
|
var PRESENCE_LAST_ACTIVITY = 0;
|
|
|
|
function getPresenceLast() {
|
|
// Last presence stamp not yet initialized?
|
|
if(PRESENCE_LAST_ACTIVITY == 0)
|
|
return 0;
|
|
|
|
return getTimeStamp() - PRESENCE_LAST_ACTIVITY;
|
|
}
|
|
|
|
// Generates the time for XMPP
|
|
function getXMPPTime(location) {
|
|
/* FROM : http://trac.jwchat.org/jsjac/browser/branches/jsjac_1.0/jsextras.js?rev=221 */
|
|
|
|
// Initialize
|
|
var jInit = new Date();
|
|
var year, month, day, hours, minutes, seconds;
|
|
|
|
// Gets the UTC date
|
|
if(location == 'utc') {
|
|
year = jInit.getUTCFullYear();
|
|
month = jInit.getUTCMonth();
|
|
day = jInit.getUTCDate();
|
|
hours = jInit.getUTCHours();
|
|
minutes = jInit.getUTCMinutes();
|
|
seconds = jInit.getUTCSeconds();
|
|
}
|
|
|
|
// Gets the local date
|
|
else {
|
|
year = jInit.getFullYear();
|
|
month = jInit.getMonth();
|
|
day = jInit.getDate();
|
|
hours = jInit.getHours();
|
|
minutes = jInit.getMinutes();
|
|
seconds = jInit.getSeconds();
|
|
}
|
|
|
|
// Generates the date string
|
|
var jDate = year + '-';
|
|
jDate += padZero(month + 1) + '-';
|
|
jDate += padZero(day) + 'T';
|
|
jDate += padZero(hours) + ':';
|
|
jDate += padZero(minutes) + ':';
|
|
jDate += padZero(seconds) + 'Z';
|
|
|
|
// Returns the date string
|
|
return jDate;
|
|
}
|
|
|
|
// Generates then human time
|
|
function getCompleteTime() {
|
|
var init = new Date();
|
|
var time = padZero(init.getHours()) + ':';
|
|
time += padZero(init.getMinutes()) + ':';
|
|
time += padZero(init.getSeconds());
|
|
|
|
return time;
|
|
}
|
|
|
|
// Gets the TZO of a date
|
|
function getDateTZO() {
|
|
// Get the date
|
|
var date = new Date();
|
|
var offset = date.getTimezoneOffset();
|
|
|
|
// Default vars
|
|
var sign = '';
|
|
var hours = 0;
|
|
var minutes = 0;
|
|
|
|
// Process a neutral offset
|
|
if(offset < 0) {
|
|
offset = offset * -1;
|
|
sign = '+';
|
|
}
|
|
|
|
// Get the values
|
|
var n_date = new Date(offset * 60 * 1000);
|
|
hours = n_date.getHours() - 1;
|
|
minutes = n_date.getMinutes();
|
|
|
|
// Process the TZO
|
|
tzo = sign + padZero(hours) + ':' + padZero(minutes);
|
|
|
|
// Return the processed value
|
|
return tzo;
|
|
}
|
|
|
|
// Parses a XMPP date (yyyy-mm-dd, hh-mm-ss) into an human-readable one
|
|
function parseDate(to_parse) {
|
|
var date = Date.jab2date(to_parse);
|
|
var parsed = date.toLocaleDateString() + ' (' + date.toLocaleTimeString() + ')';
|
|
|
|
return parsed;
|
|
}
|
|
|
|
// Parses a XMPP date (yyyy-mm-dd) into an human-readable one
|
|
function parseDay(to_parse) {
|
|
var date = Date.jab2date(to_parse);
|
|
var parsed = date.toLocaleDateString();
|
|
|
|
return parsed;
|
|
}
|
|
|
|
// Parses a XMPP date (hh-mm-ss) into an human-readable one
|
|
function parseTime(to_parse) {
|
|
var date = Date.jab2date(to_parse);
|
|
var parsed = date.toLocaleTimeString();
|
|
|
|
return parsed;
|
|
}
|
|
|
|
// Parses a XMPP date stamp into a relative one
|
|
function relativeDate(to_parse) {
|
|
// Get the current date
|
|
var current_date = Date.jab2date(getXMPPTime('utc'));
|
|
var current_day = current_date.getDate();
|
|
var current_stamp = current_date.getTime();
|
|
|
|
// Parse the given date
|
|
var old_date = Date.jab2date(to_parse);
|
|
var old_day = old_date.getDate();
|
|
var old_stamp = old_date.getTime();
|
|
var old_time = old_date.toLocaleTimeString();
|
|
|
|
// Get the day number between the two dates
|
|
var days = Math.round((current_stamp - old_stamp) / 86400000);
|
|
|
|
// Invalid date?
|
|
if(isNaN(old_stamp) || isNaN(days))
|
|
return getCompleteTime();
|
|
|
|
// Is it today?
|
|
if(current_day == old_day)
|
|
return old_time;
|
|
|
|
// It is yesterday?
|
|
if(days <= 1)
|
|
return _e("Yesterday") + ' - ' + old_time;
|
|
|
|
// Is it less than a week ago?
|
|
if(days <= 7)
|
|
return printf(_e("%s days ago"), days) + ' - ' + old_time;
|
|
|
|
// Another longer period
|
|
return old_date.toLocaleDateString() + ' - ' + old_time;
|
|
}
|
|
|
|
// Reads a message delay
|
|
function readMessageDelay(node) {
|
|
// Initialize
|
|
var delay, d_delay;
|
|
|
|
// Read the delay
|
|
d_delay = jQuery(node).find('delay[xmlns="' + NS_URN_DELAY + '"]:first').attr('stamp');
|
|
|
|
// New delay (valid XEP)
|
|
if(d_delay)
|
|
delay = d_delay;
|
|
|
|
// Old delay (obsolete XEP!)
|
|
else {
|
|
// Try to read the old-school delay
|
|
var x_delay = jQuery(node).find('x[xmlns="' + NS_DELAY + '"]:first').attr('stamp');
|
|
|
|
if(x_delay)
|
|
delay = x_delay.replace(/^(\w{4})(\w{2})(\w{2})T(\w{2}):(\w{2}):(\w{2})Z?(\S+)?/, '$1-$2-$3T$4:$5:$6Z$7');
|
|
}
|
|
|
|
return delay;
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the CAPS JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou, Maranda
|
|
Last revision: 20/02/13
|
|
|
|
*/
|
|
|
|
// Returns an array of the Jappix disco#infos
|
|
function myDiscoInfos() {
|
|
var fArray = new Array(
|
|
NS_MUC,
|
|
NS_MUC_USER,
|
|
NS_MUC_ADMIN,
|
|
NS_MUC_OWNER,
|
|
NS_MUC_CONFIG,
|
|
NS_DISCO_INFO,
|
|
NS_DISCO_ITEMS,
|
|
NS_PUBSUB_RI,
|
|
NS_BOSH,
|
|
NS_CAPS,
|
|
NS_MOOD,
|
|
NS_ACTIVITY,
|
|
NS_TUNE,
|
|
NS_GEOLOC,
|
|
NS_NICK,
|
|
NS_URN_ADATA,
|
|
NS_URN_AMETA,
|
|
NS_URN_MBLOG,
|
|
NS_URN_INBOX,
|
|
NS_MOOD + NS_NOTIFY,
|
|
NS_ACTIVITY + NS_NOTIFY,
|
|
NS_TUNE + NS_NOTIFY,
|
|
NS_GEOLOC + NS_NOTIFY,
|
|
NS_URN_MBLOG + NS_NOTIFY,
|
|
NS_URN_INBOX + NS_NOTIFY,
|
|
NS_URN_DELAY,
|
|
NS_ROSTER,
|
|
NS_ROSTERX,
|
|
NS_HTTP_AUTH,
|
|
NS_CHATSTATES,
|
|
NS_XHTML_IM,
|
|
NS_URN_MAM,
|
|
NS_IPV6,
|
|
NS_LAST,
|
|
NS_PRIVATE,
|
|
NS_REGISTER,
|
|
NS_SEARCH,
|
|
NS_COMMANDS,
|
|
NS_VERSION,
|
|
NS_XDATA,
|
|
NS_VCARD,
|
|
NS_URN_TIME,
|
|
NS_URN_PING,
|
|
NS_URN_RECEIPTS,
|
|
NS_PRIVACY,
|
|
NS_IQOOB,
|
|
NS_XOOB
|
|
);
|
|
|
|
return fArray;
|
|
}
|
|
|
|
// Gets the disco#infos of an entity
|
|
function getDiscoInfos(to, caps) {
|
|
// No CAPS
|
|
if(!caps) {
|
|
logThis('No CAPS: ' + to, 2);
|
|
|
|
displayDiscoInfos(to, '');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Get the stored disco infos
|
|
var xml = XMLFromString(getPersistent('global', 'caps', caps));
|
|
|
|
// Yet stored
|
|
if(xml) {
|
|
logThis('CAPS from cache: ' + to, 3);
|
|
|
|
displayDiscoInfos(to, xml);
|
|
|
|
return true;
|
|
}
|
|
|
|
logThis('CAPS from the network: ' + to, 3);
|
|
|
|
// Not stored: get the disco#infos
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setTo(to);
|
|
iq.setType('get');
|
|
iq.setQuery(NS_DISCO_INFO);
|
|
|
|
con.send(iq, handleDiscoInfos);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Handles the disco#infos of an entity
|
|
function handleDiscoInfos(iq) {
|
|
if(!iq || (iq.getType() == 'error'))
|
|
return;
|
|
|
|
// IQ received, get some values
|
|
var from = fullXID(getStanzaFrom(iq));
|
|
var query = iq.getQuery();
|
|
|
|
// Generate the CAPS-processing values
|
|
var identities = new Array();
|
|
var features = new Array();
|
|
var data_forms = new Array();
|
|
|
|
// Identity values
|
|
$(query).find('identity').each(function() {
|
|
var pCategory = $(this).attr('category');
|
|
var pType = $(this).attr('type');
|
|
var pLang = $(this).attr('xml:lang');
|
|
var pName = $(this).attr('name');
|
|
|
|
if(!pCategory)
|
|
pCategory = '';
|
|
if(!pType)
|
|
pType = '';
|
|
if(!pLang)
|
|
pLang = '';
|
|
if(!pName)
|
|
pName = '';
|
|
|
|
identities.push(pCategory + '/' + pType + '/' + pLang + '/' + pName);
|
|
});
|
|
|
|
// Feature values
|
|
$(query).find('feature').each(function() {
|
|
var pVar = $(this).attr('var');
|
|
|
|
// Add the current value to the array
|
|
if(pVar)
|
|
features.push(pVar);
|
|
});
|
|
|
|
// Data-form values
|
|
$(query).find('x[xmlns="' + NS_XDATA + '"]').each(function() {
|
|
// Initialize some stuffs
|
|
var pString = '';
|
|
var sortVar = new Array();
|
|
|
|
// Add the form type field
|
|
$(this).find('field[var="FORM_TYPE"] value').each(function() {
|
|
var cText = $(this).text();
|
|
|
|
if(cText)
|
|
pString += cText + '<';
|
|
});
|
|
|
|
// Add the var attributes into an array
|
|
$(this).find('field:not([var="FORM_TYPE"])').each(function() {
|
|
var cVar = $(this).attr('var');
|
|
|
|
if(cVar)
|
|
sortVar.push(cVar);
|
|
});
|
|
|
|
// Sort the var attributes
|
|
sortVar = sortVar.sort();
|
|
|
|
// Loop this sorted var attributes
|
|
for(i in sortVar) {
|
|
// Initialize the value sorting
|
|
var sortVal = new Array();
|
|
|
|
// Append it to the string
|
|
pString += sortVar[i] + '<';
|
|
|
|
// Add each value to the array
|
|
$(this).find('field[var=' + sortVar[i] + '] value').each(function() {
|
|
sortVal.push($(this).text());
|
|
});
|
|
|
|
// Sort the values
|
|
sortVal = sortVal.sort();
|
|
|
|
// Append the values to the string
|
|
for(j in sortVal)
|
|
pString += sortVal[j] + '<';
|
|
}
|
|
|
|
// Any string?
|
|
if(pString) {
|
|
// Remove the undesired double '<' from the string
|
|
if(pString.match(/(.+)(<)+$/))
|
|
pString = pString.substring(0, pString.length - 1);
|
|
|
|
// Add the current string to the array
|
|
data_forms.push(pString);
|
|
}
|
|
});
|
|
|
|
// Process the CAPS
|
|
var caps = processCaps(identities, features, data_forms);
|
|
|
|
// Get the XML string
|
|
var xml = xmlToString(query);
|
|
|
|
// Store the disco infos
|
|
setPersistent('global', 'caps', caps, xml);
|
|
|
|
// This is our server
|
|
if(from == getServer()) {
|
|
// Handle the features
|
|
handleFeatures(xml);
|
|
|
|
logThis('Got our server CAPS', 3);
|
|
}
|
|
|
|
else {
|
|
// Display the disco infos
|
|
displayDiscoInfos(from, xml);
|
|
|
|
logThis('Got CAPS: ' + from, 3);
|
|
}
|
|
}
|
|
|
|
// Displays the disco#infos everywhere needed for an entity
|
|
function displayDiscoInfos(from, xml) {
|
|
// Generate the chat path
|
|
var xid = bareXID(from);
|
|
|
|
// This comes from a private groupchat chat?
|
|
if(isPrivate(xid))
|
|
xid = from;
|
|
|
|
hash = hex_md5(xid);
|
|
|
|
// Support indicators
|
|
var xhtml_im = false;
|
|
var iq_oob = false;
|
|
var x_oob = false;
|
|
var receipts = false;
|
|
|
|
// Display the supported features
|
|
$(xml).find('feature').each(function() {
|
|
var current = $(this).attr('var');
|
|
|
|
// xHTML-IM
|
|
if(current == NS_XHTML_IM)
|
|
xhtml_im = true;
|
|
|
|
// Out of Band Data (IQ)
|
|
if(current == NS_IQOOB)
|
|
iq_oob = true;
|
|
|
|
// Out of Band Data (X)
|
|
if(current == NS_XOOB)
|
|
x_oob = true;
|
|
|
|
// Receipts
|
|
else if(current == NS_URN_RECEIPTS)
|
|
receipts = true;
|
|
});
|
|
|
|
// Paths
|
|
var path = $('#' + hash);
|
|
var message_area = path.find('.message-area');
|
|
var style = path.find('.chat-tools-style');
|
|
var file = path.find('.chat-tools-file');
|
|
|
|
// Apply xHTML-IM
|
|
if(xhtml_im)
|
|
style.show();
|
|
else {
|
|
// Remove the tooltip elements
|
|
style.hide();
|
|
style.find('.bubble-style').remove();
|
|
|
|
// Reset the markers
|
|
message_area.removeAttr('style')
|
|
.removeAttr('data-font')
|
|
.removeAttr('data-fontsize')
|
|
.removeAttr('data-color')
|
|
.removeAttr('data-bold')
|
|
.removeAttr('data-italic')
|
|
.removeAttr('data-underline');
|
|
}
|
|
|
|
// Apply Out of Band Data
|
|
if(iq_oob || x_oob) {
|
|
file.show();
|
|
|
|
// Set a marker
|
|
if(iq_oob)
|
|
file.attr('data-oob', 'iq');
|
|
else
|
|
file.attr('data-oob', 'x');
|
|
}
|
|
|
|
else {
|
|
// Remove the tooltip elements
|
|
file.hide();
|
|
file.find('.bubble-style').remove();
|
|
|
|
// Reset the marker
|
|
file.removeAttr('data-oob');
|
|
}
|
|
|
|
// Apply receipts
|
|
if(receipts)
|
|
message_area.attr('data-receipts', 'true');
|
|
else
|
|
message_area.removeAttr('data-receipts');
|
|
}
|
|
|
|
// Generates the CAPS hash
|
|
function processCaps(cIdentities, cFeatures, cDataForms) {
|
|
// Initialize
|
|
var cString = '';
|
|
|
|
// Sort the arrays
|
|
cIdentities = cIdentities.sort();
|
|
cFeatures = cFeatures.sort();
|
|
cDataForms = cDataForms.sort();
|
|
|
|
// Process the sorted identity string
|
|
for(a in cIdentities)
|
|
cString += cIdentities[a] + '<';
|
|
|
|
// Process the sorted feature string
|
|
for(b in cFeatures)
|
|
cString += cFeatures[b] + '<';
|
|
|
|
// Process the sorted data-form string
|
|
for(c in cDataForms)
|
|
cString += cDataForms[c] + '<';
|
|
|
|
// Process the SHA-1 hash
|
|
var cHash = b64_sha1(cString);
|
|
|
|
return cHash;
|
|
}
|
|
|
|
// Generates the Jappix CAPS hash
|
|
function myCaps() {
|
|
return processCaps(new Array('client/web//Jappix'), myDiscoInfos(), new Array());
|
|
}
|
|
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the vCard JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou, Maranda
|
|
Last revision: 20/02/12
|
|
|
|
*/
|
|
|
|
// Opens the vCard popup
|
|
function openVCard() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Your profile") + '</div>' +
|
|
|
|
'<div class="tab">' +
|
|
'<a href="#" class="tab-active" data-key="1">' + _e("Identity") + '</a>' +
|
|
'<a href="#" data-key="2">' + _e("Profile image") + '</a>' +
|
|
'<a href="#" data-key="3">' + _e("Others") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div id="lap1" class="lap-active one-lap forms">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Personal") + '</legend>' +
|
|
|
|
'<label for="USER-FN">' + _e("Complete name") + '</label>' +
|
|
'<input type="text" id="USER-FN" class="vcard-item" placeholder="John Locke" />' +
|
|
|
|
'<label for="USER-NICKNAME">' + _e("Nickname") + '</label>' +
|
|
'<input type="text" id="USER-NICKNAME" class="vcard-item" placeholder="Jo" />' +
|
|
|
|
'<label for="USER-N-GIVEN">' + _e("First name") + '</label>' +
|
|
'<input type="text" id="USER-N-GIVEN" class="vcard-item" placeholder="John" />' +
|
|
|
|
'<label for="USER-N-FAMILY">' + _e("Last name") + '</label>' +
|
|
'<input type="text" id="USER-N-FAMILY" class="vcard-item" placeholder="Locke" />' +
|
|
|
|
'<label for="USER-BDAY">' + _e("Date of birth") + '</label>' +
|
|
'<input type="text" id="USER-BDAY" class="vcard-item" pattern="^[0-9]{2}-[0-9]{2}-[0-9]{4}$" placeholder="16-02-1974" />' +
|
|
'</fieldset>' +
|
|
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Contact") + '</legend>' +
|
|
|
|
'<label for="USER-EMAIL-USERID">' + _e("E-mail") + '</label>' +
|
|
'<input type="text" id="USER-EMAIL-USERID" class="vcard-item" placeholder="john@locke.fam" />' +
|
|
|
|
'<label for="USER-TEL-NUMBER">' + _e("Phone") + '</label>' +
|
|
'<input type="text" id="USER-TEL-NUMBER" class="vcard-item" placeholder="John" placeholder="+1-292-321-0812" />' +
|
|
|
|
'<label for="USER-URL">' + _e("Website") + '</label>' +
|
|
'<input type="text" id="USER-URL" class="vcard-item" placeholder="john.locke.fam" />' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
|
|
'<div id="lap2" class="one-lap forms">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("New") + '</legend>' +
|
|
|
|
'<input type="hidden" id="USER-PHOTO-TYPE" class="vcard-item" />' +
|
|
'<input type="hidden" id="USER-PHOTO-BINVAL" class="vcard-item" />' +
|
|
|
|
'<form id="vcard-avatar" action="./php/avatar-upload.php" method="post" enctype="multipart/form-data">' +
|
|
generateFileShare() +
|
|
'</form>' +
|
|
'</fieldset>' +
|
|
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Current") + '</legend>' +
|
|
|
|
'<div class="avatar-container"></div>' +
|
|
|
|
'<a href="#" class="one-button avatar-delete talk-images">' + _e("Delete") + '</a>' +
|
|
'<div class="no-avatar">' + _e("What a pity! You have no profile image defined in your identity card!") + '</div>' +
|
|
'</fieldset>' +
|
|
|
|
'<div class="avatar-wait avatar-info">' + _e("Please wait while your avatar is uploaded...") + '</div>' +
|
|
'<div class="avatar-ok avatar-info">' + _e("Here it is! A new beautiful profile image!") + '</div>' +
|
|
'<div class="avatar-error avatar-info">' + _e("The image file is not supported or has a bad size.") + '</div>' +
|
|
'</div>' +
|
|
|
|
'<div id="lap3" class="one-lap forms">' +
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Address") + '</legend>' +
|
|
|
|
'<label for="USER-ADR-STREET">' + _e("Street") + '</label>' +
|
|
'<input type="text" id="USER-ADR-STREET" class="vcard-item" placeholder="Manhattan" />' +
|
|
|
|
'<label for="USER-ADR-LOCALITY">' + _e("City") + '</label>' +
|
|
'<input type="text" id="USER-ADR-LOCALITY" class="vcard-item" placeholder="New-York" />' +
|
|
|
|
'<label for="USER-ADR-PCODE">' + _e("Postal code") + '</label>' +
|
|
'<input type="text" id="USER-ADR-PCODE" class="vcard-item" placeholder="10002" />' +
|
|
|
|
'<label for="USER-ADR-CTRY">' + _e("Country") + '</label>' +
|
|
'<input type="text" id="USER-ADR-CTRY" class="vcard-item" placeholder="USA" />' +
|
|
'</fieldset>' +
|
|
|
|
'<fieldset>' +
|
|
'<legend>' + _e("Biography") + '</legend>' +
|
|
|
|
'<textarea id="USER-DESC" rows="8" cols="60" class="vcard-item"></textarea>' +
|
|
'</fieldset>' +
|
|
'</div>' +
|
|
|
|
'<div class="infos">' +
|
|
'<p class="infos-title">' + _e("Important notice") + '</p>' +
|
|
|
|
'<p>' + _e("Be careful with the information you store into your profile, because it might be accessible by everyone (even someone you don't want to).") + '</p>' +
|
|
'<p>' + _e("Not everything is private on XMPP; this is one of those things, your public profile (vCard).") + '</p>' +
|
|
'<p>' + printf(_e("It is strongly recommended to upload a profile image (%s maximum), like a picture of yourself, because that makes you easily recognizable by your friends."), JAPPIX_MAX_UPLOAD) + '</p>' +
|
|
'<p><b><a href="https://me.jappix.com/new" target="_blank">' + _e("Enable my public profile") + ' »</a></b></p>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish save disabled">' + _e("Save") + '</a>' +
|
|
'<a href="#" class="finish cancel">' + _e("Cancel") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('vcard', html);
|
|
|
|
// Associate the events
|
|
launchVCard();
|
|
|
|
// We get the VCard informations
|
|
getVCard(getXID(), 'user');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Closes the vCard popup
|
|
function closeVCard() {
|
|
// Destroy the popup
|
|
destroyPopup('vcard');
|
|
|
|
// Create the welcome end popup?
|
|
if(END_WELCOME) {
|
|
// Open popup
|
|
openMe();
|
|
|
|
// Unavoidable popup
|
|
$('#me').addClass('unavoidable');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Switches the vCard popup tabs
|
|
function switchVCard(id) {
|
|
$('#vcard .one-lap').removeClass('lap-active');
|
|
$('#vcard #lap' + id).addClass('lap-active');
|
|
$('#vcard .tab a').removeClass('tab-active');
|
|
$('#vcard .tab a[data-key="' + id + '"]').addClass('tab-active');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Waits for the avatar upload reply
|
|
function waitAvatarUpload() {
|
|
// Reset the avatar info
|
|
$('#vcard .avatar-info').hide().stopTime();
|
|
|
|
// Show the wait info
|
|
$('#vcard .avatar-wait').show();
|
|
}
|
|
|
|
// Handles the avatar upload reply
|
|
function handleAvatarUpload(responseXML) {
|
|
// Data selector
|
|
var dData = $(responseXML).find('jappix');
|
|
|
|
// Not current upload session?
|
|
if(parseInt(dData.attr('id')) != parseInt($('#vcard-avatar input[name="id"]').val()))
|
|
return;
|
|
|
|
// Reset the avatar info
|
|
$('#vcard .avatar-info').hide().stopTime();
|
|
|
|
// Process the returned data
|
|
if(!dData.find('error').size()) {
|
|
// Read the values
|
|
var aType = dData.find('type').text();
|
|
var aBinval = dData.find('binval').text();
|
|
|
|
// We remove everything that isn't useful right here
|
|
$('#vcard .no-avatar').hide();
|
|
$('#vcard .avatar').remove();
|
|
|
|
// We display the delete button
|
|
$('#vcard .avatar-delete').show();
|
|
|
|
// We tell the user it's okay
|
|
$('#vcard .avatar-ok').show();
|
|
|
|
// Timer
|
|
$('#vcard .avatar-info').oneTime('10s', function() {
|
|
$(this).hide();
|
|
});
|
|
|
|
// We put the base64 values in a hidden input to be sent
|
|
$('#USER-PHOTO-TYPE').val(aType);
|
|
$('#USER-PHOTO-BINVAL').val(aBinval);
|
|
|
|
// We display the avatar !
|
|
$('#vcard .avatar-container').replaceWith('<div class="avatar-container"><img class="avatar" src="data:' + aType + ';base64,' + aBinval + '" alt="" /></div>');
|
|
}
|
|
|
|
// Any error?
|
|
else {
|
|
$('#vcard .avatar-error').show();
|
|
|
|
// Timer
|
|
$('#vcard .avatar-info').oneTime('10s', function() {
|
|
$(this).hide();
|
|
});
|
|
|
|
logThis('Error while uploading the avatar: ' + dData.find('error').text(), 1);
|
|
}
|
|
}
|
|
|
|
// Deletes the encoded avatar of an user
|
|
function deleteAvatar() {
|
|
// We remove the avatar displayed elements
|
|
$('#vcard .avatar-info').stopTime();
|
|
$('#vcard .avatar-info, #vcard .avatar-wait, #vcard .avatar-error, #vcard .avatar-ok, #vcard .avatar-delete').hide();
|
|
$('#vcard .avatar').remove();
|
|
|
|
// We reset the input value
|
|
$('#USER-PHOTO-TYPE, #USER-PHOTO-BINVAL').val('');
|
|
|
|
// We show the avatar-uploading request
|
|
$('#vcard .no-avatar').show();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Creates a special vCard input
|
|
function createInputVCard(id, type) {
|
|
// Generate the new ID
|
|
id = 'USER-' + id;
|
|
|
|
// Can append the content
|
|
if((type == 'user') && !exists('#vcard #' + id))
|
|
$('#vcard .content').append('<input id="' + id + '" class="vcard-item" type="hidden" />');
|
|
}
|
|
|
|
// Gets the vCard of a XID
|
|
function getVCard(to, type) {
|
|
// Generate a special ID
|
|
var id = genID();
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setID(id);
|
|
iq.setType('get');
|
|
iq.appendNode('vCard', {'xmlns': NS_VCARD});
|
|
|
|
// Send the IQ to the good user
|
|
if(type == 'user') {
|
|
// Show the wait icon
|
|
$('#vcard .wait').show();
|
|
|
|
// Apply the session ID
|
|
$('#vcard').attr('data-vcard', id);
|
|
|
|
// Send the IQ
|
|
con.send(iq, handeUVCard);
|
|
}
|
|
|
|
else {
|
|
// Show the wait icon
|
|
$('#userinfos .wait').show();
|
|
|
|
// Apply the session ID
|
|
$('#userinfos').attr('data-vcard', id);
|
|
|
|
// Send the IQ
|
|
iq.setTo(to);
|
|
con.send(iq, handeBVCard);
|
|
}
|
|
}
|
|
|
|
// Handles the current connected user's vCard
|
|
function handeUVCard(iq) {
|
|
handleVCard(iq, 'user');
|
|
}
|
|
|
|
// Handles an external buddy vCard
|
|
function handeBVCard(iq) {
|
|
handleVCard(iq, 'buddy');
|
|
}
|
|
|
|
// Handles a vCard stanza
|
|
function handleVCard(iq, type) {
|
|
// Extract the data
|
|
var iqID = iq.getID();
|
|
var iqFrom = fullXID(getStanzaFrom(iq));
|
|
var iqNode = iq.getNode();
|
|
|
|
// Define some paths
|
|
var path_vCard = '#vcard[data-vcard="' + iqID + '"]';
|
|
var path_userInfos = '#userinfos[data-vcard="' + iqID + '"]';
|
|
|
|
// End if the session does not exist
|
|
if(((type == 'user') && !exists(path_vCard)) || ((type == 'buddy') && !exists(path_userInfos)))
|
|
return;
|
|
|
|
// We retrieve main values
|
|
var values_yet = [];
|
|
|
|
$(iqNode).find('vCard').children().each(function() {
|
|
// Read the current parent node name
|
|
var tokenname = (this).nodeName.toUpperCase();
|
|
|
|
// Node with a parent
|
|
if($(this).children().size()) {
|
|
$(this).children().each(function() {
|
|
// Get the node values
|
|
var currentID = tokenname + '-' + (this).nodeName.toUpperCase();
|
|
var currentText = $(this).text();
|
|
|
|
// Not yet added?
|
|
if(!existArrayValue(values_yet, currentID)) {
|
|
// Create an input if it does not exist
|
|
createInputVCard(values_yet, type);
|
|
|
|
// Userinfos viewer popup
|
|
if((type == 'buddy') && currentText) {
|
|
if(currentID == 'EMAIL-USERID')
|
|
$(path_userInfos + ' #BUDDY-' + currentID).html('<a href="mailto:' + currentText.htmlEnc() + '" target="_blank">' + currentText.htmlEnc() + '</a>');
|
|
else
|
|
$(path_userInfos + ' #BUDDY-' + currentID).text(currentText.htmlEnc());
|
|
}
|
|
|
|
// Profile editor popup
|
|
else if(type == 'user')
|
|
$(path_vCard + ' #USER-' + currentID).val(currentText);
|
|
|
|
// Avoid duplicating the value
|
|
values_yet.push(currentID);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Node without any parent
|
|
else {
|
|
// Get the node values
|
|
var currentText = $(this).text();
|
|
|
|
// Not yet added?
|
|
if(!existArrayValue(values_yet, tokenname)) {
|
|
// Create an input if it does not exist
|
|
createInputVCard(tokenname, type);
|
|
|
|
// Userinfos viewer popup
|
|
if((type == 'buddy') && currentText) {
|
|
// URL modification
|
|
if(tokenname == 'URL') {
|
|
// No http:// or https:// prefix, we should add it
|
|
if(!currentText.match(/^https?:\/\/(.+)/))
|
|
currentText = 'http://' + currentText;
|
|
|
|
currentText = '<a href="' + currentText + '" target="_blank">' + currentText.htmlEnc() + '</a>';
|
|
}
|
|
|
|
// Description modification
|
|
else if(tokenname == 'DESC')
|
|
currentText = filterThisMessage(currentText, getBuddyName(iqFrom).htmlEnc(), true);
|
|
|
|
// Other stuffs
|
|
else
|
|
currentText = currentText.htmlEnc();
|
|
|
|
$(path_userInfos + ' #BUDDY-' + tokenname).html(currentText);
|
|
}
|
|
|
|
// Profile editor popup
|
|
else if(type == 'user')
|
|
$(path_vCard + ' #USER-' + tokenname).val(currentText);
|
|
|
|
// Avoid duplicating the value
|
|
values_yet.push(tokenname);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update the stored avatar
|
|
if(type == 'buddy') {
|
|
// Get the avatar XML
|
|
var xml = getPersistent('global', 'avatar', iqFrom);
|
|
|
|
// If there were no stored avatar previously
|
|
if($(XMLFromString(xml)).find('type').text() == 'none') {
|
|
xml = xml.replace(/<forced>false<\/forced>/gi, '<forced>true</forced>');
|
|
setPersistent(getXID(), 'avatar', iqFrom, xml);
|
|
}
|
|
|
|
// Handle the user avatar
|
|
handleAvatar(iq);
|
|
}
|
|
|
|
// The avatar values targets
|
|
var aBinval, aType, aContainer;
|
|
|
|
if(type == 'user') {
|
|
aBinval = $('#USER-PHOTO-BINVAL').val();
|
|
aType = $('#USER-PHOTO-TYPE').val();
|
|
aContainer = path_vCard + ' .avatar-container';
|
|
}
|
|
|
|
else {
|
|
aBinval = $(iqNode).find('BINVAL:first').text();
|
|
aType = $(iqNode).find('TYPE:first').text();
|
|
aContainer = path_userInfos + ' .avatar-container';
|
|
}
|
|
|
|
// We display the avatar if retrieved
|
|
if(aBinval) {
|
|
// No type?
|
|
if(!aType)
|
|
aType = 'image/png';
|
|
|
|
if(type == 'user') {
|
|
// We move all the things that we don't need in that case
|
|
$(path_vCard + ' .no-avatar').hide();
|
|
$(path_vCard + ' .avatar-delete').show();
|
|
$(path_vCard + ' .avatar').remove();
|
|
}
|
|
|
|
// We display the avatar we have just received
|
|
$(aContainer).replaceWith('<div class="avatar-container"><img class="avatar" src="data:' + aType + ';base64,' + aBinval + '" alt="" /></div>');
|
|
}
|
|
|
|
else if(type == 'buddy')
|
|
$(aContainer).replaceWith('<div class="avatar-container"><img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" /></div>');
|
|
|
|
// Do someting depending of the type
|
|
if(type == 'user') {
|
|
$(path_vCard + ' .wait').hide();
|
|
$(path_vCard + ' .finish:first').removeClass('disabled');
|
|
}
|
|
|
|
else
|
|
vCardBuddyInfos();
|
|
|
|
logThis('vCard received: ' + iqFrom);
|
|
}
|
|
|
|
// Sends the vCard of the user
|
|
function sendVCard() {
|
|
// Initialize the IQ
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var vCard = iq.appendNode('vCard', {'xmlns': NS_VCARD});
|
|
|
|
// We send the identity part of the form
|
|
$('#vcard .vcard-item').each(function() {
|
|
var itemID = $(this).attr('id').replace(/^USER-(.+)/, '$1');
|
|
var itemVal = $(this).val();
|
|
|
|
if(itemVal && itemID) {
|
|
if(itemID.indexOf('-') != -1) {
|
|
var tagname = explodeThis('-', itemID, 0);
|
|
var aNode;
|
|
|
|
if(vCard.getElementsByTagName(tagname).length > 0)
|
|
aNode = vCard.getElementsByTagName(tagname).item(0);
|
|
else
|
|
aNode = vCard.appendChild(iq.buildNode(tagname, {'xmlns': NS_VCARD}));
|
|
|
|
aNode.appendChild(iq.buildNode(explodeThis('-', itemID, 1), {'xmlns': NS_VCARD}, itemVal));
|
|
}
|
|
|
|
else
|
|
vCard.appendChild(iq.buildNode(itemID, {'xmlns': NS_VCARD}, itemVal));
|
|
}
|
|
});
|
|
|
|
// Send the IQ
|
|
con.send(iq);
|
|
|
|
// Send the user nickname & avatar over PEP
|
|
if(enabledPEP()) {
|
|
// Read values
|
|
var user_nick = $('#USER-NICKNAME').val();
|
|
var photo_bin = $('#USER-PHOTO-BINVAL').val();
|
|
var photo_type = $('#USER-PHOTO-TYPE').val();
|
|
var photo_data = Base64.decode(photo_bin) || '';
|
|
var photo_bytes = photo_data.length || '';
|
|
var photo_id = hex_sha1(photo_data) || '';
|
|
|
|
// Values array
|
|
var read = [
|
|
user_nick,
|
|
photo_bin,
|
|
[photo_type, photo_id, photo_bytes]
|
|
];
|
|
|
|
// Nodes array
|
|
var node = [
|
|
NS_NICK,
|
|
NS_URN_ADATA,
|
|
NS_URN_AMETA
|
|
];
|
|
|
|
// Generate the XML
|
|
for(i in read) {
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB});
|
|
var publish = pubsub.appendChild(iq.buildNode('publish', {'node': node[i], 'xmlns': NS_PUBSUB}));
|
|
|
|
if((i == 0) && read[0]) {
|
|
var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB}));
|
|
|
|
// Nickname element
|
|
item.appendChild(iq.buildNode('nick', {'xmlns': NS_NICK}, read[i]));
|
|
}
|
|
|
|
else if(((i == 1) || (i == 2)) && read[1]) {
|
|
var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB}));
|
|
|
|
// Apply the SHA-1 hash
|
|
if(photo_id)
|
|
item.setAttribute('id', photo_id);
|
|
|
|
// Avatar data node
|
|
if(i == 1)
|
|
item.appendChild(iq.buildNode('data', {'xmlns': NS_URN_ADATA}, read[i]));
|
|
|
|
// Avatar metadata node
|
|
else {
|
|
var metadata = item.appendChild(iq.buildNode('metadata', {'xmlns': NS_URN_AMETA}));
|
|
|
|
if(read[i]) {
|
|
var meta_info = metadata.appendChild(iq.buildNode('info', {'xmlns': NS_URN_AMETA}));
|
|
|
|
if(read[i][0])
|
|
meta_info.setAttribute('type', read[i][0]);
|
|
if(read[i][1])
|
|
meta_info.setAttribute('id', read[i][1]);
|
|
if(read[i][2])
|
|
meta_info.setAttribute('bytes', read[i][2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
con.send(iq);
|
|
}
|
|
}
|
|
|
|
// Close the vCard stuffs
|
|
closeVCard();
|
|
|
|
// Get our new avatar
|
|
getAvatar(getXID(), 'force', 'true', 'forget');
|
|
|
|
logThis('vCard sent.');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchVCard() {
|
|
// Focus on the first input
|
|
$(document).oneTime(10, function() {
|
|
$('#vcard input:first').focus();
|
|
});
|
|
|
|
// Keyboard events
|
|
$('#vcard input[type="text"]').keyup(function(e) {
|
|
// Enter pressed: send the vCard
|
|
if((e.keyCode == 13) && !$('#vcard .finish.save').hasClass('disabled'))
|
|
return sendVCard();
|
|
});
|
|
|
|
// Click events
|
|
$('#vcard .tab a').click(function() {
|
|
// Yet active?
|
|
if($(this).hasClass('tab-active'))
|
|
return false;
|
|
|
|
// Switch to the good tab
|
|
var key = parseInt($(this).attr('data-key'));
|
|
|
|
return switchVCard(key);
|
|
});
|
|
|
|
$('#vcard .avatar-delete').click(function() {
|
|
return deleteAvatar();
|
|
});
|
|
|
|
$('#vcard .bottom .finish').click(function() {
|
|
if($(this).is('.cancel'))
|
|
return closeVCard();
|
|
if($(this).is('.save') && !$(this).hasClass('disabled'))
|
|
return sendVCard();
|
|
|
|
return false;
|
|
});
|
|
|
|
// Avatar upload vars
|
|
var avatar_options = {
|
|
dataType: 'xml',
|
|
beforeSubmit: waitAvatarUpload,
|
|
success: handleAvatarUpload
|
|
};
|
|
|
|
// Avatar upload form submit event
|
|
$('#vcard-avatar').submit(function() {
|
|
if($('#vcard .wait').is(':hidden') && $('#vcard .avatar-info.avatar-wait').is(':hidden') && $('#vcard-avatar input[type="file"]').val())
|
|
$(this).ajaxSubmit(avatar_options);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Avatar upload input change event
|
|
$('#vcard-avatar input[type="file"]').change(function() {
|
|
if($('#vcard .wait').is(':hidden') && $('#vcard .avatar-info.avatar-wait').is(':hidden') && $(this).val())
|
|
$('#vcard-avatar').ajaxSubmit(avatar_options);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Placeholders
|
|
$('#vcard-avatar input[type="text"]').placeholder();
|
|
}
|
|
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the user-infos JS scripts for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 27/03/11
|
|
|
|
*/
|
|
|
|
// Opens the user-infos popup
|
|
function openUserInfos(xid) {
|
|
// Can show shortcuts?
|
|
var shortcuts = '';
|
|
|
|
if(xid != getXID()) {
|
|
shortcuts = '<div class="shortcuts">' +
|
|
'<a href="#" class="message talk-images" title="' + _e("Send him/her a message") + '" onclick="closeUserInfos(); return composeInboxMessage(\'' + encodeOnclick(xid) + '\');"></a>' +
|
|
'<a href="#" class="chat talk-images" title="' + _e("Start a chat with him/her") + '" onclick="closeUserInfos(); return checkChatCreate(\'' + encodeOnclick(xid) + '\', \'chat\');"></a>' +
|
|
'<a href="#" class="command talk-images" title="' + _e("Command") + '" onclick="closeUserInfos(); return retrieveAdHoc(\'' + encodeOnclick(xid) + '\');"></a>' +
|
|
'</div>';
|
|
}
|
|
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("User profile") + '</div>' +
|
|
|
|
'<div class="tab">' +
|
|
'<a href="#" class="tab-active" data-key="1">' + _e("General") + '</a>' +
|
|
'<a href="#" data-key="2">' + _e("Advanced") + '</a>' +
|
|
'<a href="#" data-key="3">' + _e("Comments") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="lap-active one-lap info1">' +
|
|
'<div class="main-infos">' +
|
|
'<div class="avatar-container">' +
|
|
'<img class="avatar" src="' + './img/others/default-avatar.png' + '" alt="" />' +
|
|
'</div>' +
|
|
|
|
'<h1 id="BUDDY-FN" class="reset-info">' + _e("unknown") + '</h1>' +
|
|
'<h2 class="buddy-xid" class="reset-info">' + _e("unknown") + '</h2>' +
|
|
'<h3 class="buddy-last" class="reset-info">' + _e("unknown") + '</h3>' +
|
|
|
|
shortcuts +
|
|
'</div>' +
|
|
|
|
'<div class="block-infos">' +
|
|
'<div class="one-line"><b class="line-label">' + _e("Date of birth") + '</b><span id="BUDDY-BDAY" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("E-mail") + '</b><span id="BUDDY-EMAIL-USERID" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("Phone") + '</b><span id="BUDDY-TEL-NUMBER" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("Website") + '</b><span id="BUDDY-URL" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="block-infos">' +
|
|
'<div class="one-line"><b class="line-label">' + _e("Client") + '</b><span id="BUDDY-CLIENT" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("System") + '</b><span id="BUDDY-SYSTEM" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("Local time") + '</b><span id="BUDDY-TIME" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="one-lap info2">' +
|
|
'<div class="block-infos">' +
|
|
'<div class="one-line"><b class="line-label">' + _e("Street") + '</b><span id="BUDDY-ADR-STREET" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("City") + '</b><span id="BUDDY-ADR-LOCALITY" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("Postal code") + '</b><span id="BUDDY-ADR-PCODE" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
|
|
'<div class="one-line"><b class="line-label">' + _e("Country") + '</b><span id="BUDDY-ADR-CTRY" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="block-infos">' +
|
|
'<div class="one-line"><b class="line-label">' + _e("Biography") + '</b><span id="BUDDY-DESC" class="reset-info">' + _e("unknown") + '</span></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="one-lap info3">' +
|
|
'<textarea class="rosternotes" rows="8" cols="60"></textarea>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('userinfos', html);
|
|
|
|
// Associate the events
|
|
launchUserInfos();
|
|
|
|
// We retrieve the user's vcard
|
|
retrieveUserInfos(xid);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Closes the user-infos popup
|
|
function closeUserInfos() {
|
|
// Send the buddy comments
|
|
sendBuddyComments();
|
|
|
|
// Destroy the popup
|
|
destroyPopup('userinfos');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the user-infos
|
|
function retrieveUserInfos(xid) {
|
|
// We setup the waiting indicator
|
|
markers = 'vcard last';
|
|
|
|
// We put the user's XID
|
|
$('#userinfos .buddy-xid').text(xid);
|
|
|
|
// We get the vCard
|
|
getVCard(xid, 'buddy');
|
|
|
|
// Get the highest resource for this XID
|
|
var cXID = highestPriority(xid);
|
|
var pXID = xid;
|
|
|
|
// If the user is logged in
|
|
if(cXID) {
|
|
// Change the XID
|
|
pXID = cXID;
|
|
|
|
// We request the user's system infos
|
|
queryUserInfos(cXID, 'version')
|
|
|
|
// We request the user's local time
|
|
queryUserInfos(cXID, 'time')
|
|
|
|
// Add these to the markers
|
|
markers += ' version time';
|
|
}
|
|
|
|
// We request the user's last activity
|
|
queryUserInfos(pXID, 'last');
|
|
|
|
// Add the markers
|
|
$('#userinfos .content').addClass(markers);
|
|
|
|
// We request all the user's comments
|
|
displayBuddyComments(xid);
|
|
}
|
|
|
|
// Builds the asked user-infos query
|
|
function queryUserInfos(xid, mode) {
|
|
// Generate a session ID
|
|
var id = genID();
|
|
$('#userinfos').attr('data-' + mode, id);
|
|
|
|
// New IQ
|
|
var iq = new JSJaCIQ();
|
|
|
|
iq.setID(id);
|
|
iq.setType('get');
|
|
iq.setTo(xid);
|
|
|
|
// Last activity query
|
|
if(mode == 'last') {
|
|
iq.setQuery(NS_LAST);
|
|
con.send(iq, lastActivityUserInfos);
|
|
}
|
|
|
|
// Time query
|
|
else if(mode == 'time') {
|
|
iq.appendNode('time', {'xmlns': NS_URN_TIME});
|
|
con.send(iq, localTimeUserInfos);
|
|
}
|
|
|
|
// Version query
|
|
else if(mode == 'version') {
|
|
iq.setQuery(NS_VERSION);
|
|
con.send(iq, versionUserInfos);
|
|
}
|
|
}
|
|
|
|
// Checks if the waiting item can be hidden
|
|
function vCardBuddyInfos() {
|
|
$('#userinfos .content').removeClass('vcard');
|
|
wUserInfos();
|
|
}
|
|
|
|
// Displays the buddy comments
|
|
function displayBuddyComments(xid) {
|
|
// We get the value in the database
|
|
var value = getDB('rosternotes', xid);
|
|
|
|
// Display the value
|
|
if(value)
|
|
$('#userinfos .rosternotes').val(value);
|
|
}
|
|
|
|
// Displays the user's last activity result
|
|
function lastActivityUserInfos(iq) {
|
|
// Extract the request ID
|
|
var id = iq.getID();
|
|
var path = '#userinfos[data-last="' + id + '"]';
|
|
|
|
// End if session does not exist
|
|
if(!exists(path))
|
|
return;
|
|
|
|
if(iq && (iq.getType() == 'result')) {
|
|
// Get the values
|
|
var from = fullXID(getStanzaFrom(iq));
|
|
var seconds = $(iq.getNode()).find('query').attr('seconds');
|
|
|
|
// Any seconds?
|
|
if(seconds != undefined) {
|
|
// Initialize the parsing
|
|
var last;
|
|
seconds = parseInt(seconds);
|
|
|
|
// Active user
|
|
if(seconds <= 60)
|
|
last = _e("User currently active");
|
|
|
|
// Inactive user
|
|
else {
|
|
// Parse the date
|
|
var date_now = new Date();
|
|
var time_now = date_now.getTime();
|
|
var date_last = new Date(date_now - (seconds * 1000));
|
|
var date = date_last.toLocaleString();
|
|
|
|
// Offline user
|
|
if(from.indexOf('/') == -1)
|
|
last = printf(_e("Last seen: %s"), date);
|
|
|
|
// Online user
|
|
else
|
|
last = printf(_e("Inactive since: %s"), date);
|
|
}
|
|
|
|
// Append this text
|
|
$('#userinfos .buddy-last').text(last);
|
|
}
|
|
|
|
logThis('Last activity received: ' + from);
|
|
}
|
|
|
|
$('#userinfos .content').removeClass('last');
|
|
wUserInfos();
|
|
}
|
|
|
|
// Displays the user's software version result
|
|
function versionUserInfos(iq) {
|
|
// Extract the request ID
|
|
var id = iq.getID();
|
|
var path = '#userinfos[data-version="' + id + '"]';
|
|
|
|
// End if session does not exist
|
|
if(!exists(path))
|
|
return;
|
|
|
|
// Extract the reply data
|
|
if(iq && (iq.getType() == 'result')) {
|
|
// Get the values
|
|
var xml = iq.getQuery();
|
|
var name = $(xml).find('name').text();
|
|
var version = $(xml).find('version').text();
|
|
var os = $(xml).find('os').text();
|
|
|
|
// Put the values together
|
|
if(name && version)
|
|
name = name + ' ' + version;
|
|
|
|
// Display the values
|
|
if(name)
|
|
$(path + ' #BUDDY-CLIENT').text(name);
|
|
if(os)
|
|
$(path + ' #BUDDY-SYSTEM').text(os);
|
|
|
|
logThis('Software version received: ' + fullXID(getStanzaFrom(iq)));
|
|
}
|
|
|
|
$('#userinfos .content').removeClass('version');
|
|
wUserInfos();
|
|
}
|
|
|
|
// Displays the user's local time result
|
|
function localTimeUserInfos(iq) {
|
|
// Extract the request ID
|
|
var id = iq.getID();
|
|
var path = '#userinfos[data-time="' + id + '"]';
|
|
|
|
// End if session does not exist
|
|
if(!exists(path))
|
|
return;
|
|
|
|
if(iq && (iq.getType() == 'result')) {
|
|
// Get the values
|
|
var xml = iq.getNode();
|
|
var tzo = $(xml).find('tzo').text();
|
|
var utc = $(xml).find('utc').text();
|
|
|
|
// Any UTC?
|
|
if(utc) {
|
|
// Add the TZO if there's no one
|
|
if(tzo && utc.match(/^(.+)Z$/))
|
|
utc = RegExp.$1 + tzo;
|
|
|
|
// Get the local date string
|
|
var local_string = Date.hrTime(utc);
|
|
|
|
// Then display it
|
|
$(path + ' #BUDDY-TIME').text(local_string);
|
|
}
|
|
|
|
logThis('Local time received: ' + fullXID(getStanzaFrom(iq)));
|
|
}
|
|
|
|
$('#userinfos .content').removeClass('time');
|
|
wUserInfos();
|
|
}
|
|
|
|
// Hides the waiting image if needed
|
|
function wUserInfos() {
|
|
var selector = $('#userinfos .content');
|
|
|
|
if(!selector.hasClass('vcard') && !selector.hasClass('last') && !selector.hasClass('version') && !selector.hasClass('time'))
|
|
$('#userinfos .wait').hide();
|
|
}
|
|
|
|
// Sends the buddy comments
|
|
function sendBuddyComments() {
|
|
// Update the current value
|
|
var value = $('#userinfos .rosternotes').val();
|
|
var xid = $('#userinfos .buddy-xid').text();
|
|
|
|
// Necessary to update?
|
|
var old_value = getDB('rosternotes', xid);
|
|
|
|
if((old_value == value) || (!old_value && !value))
|
|
return false;
|
|
|
|
// Update the database
|
|
setDB('rosternotes', xid, value);
|
|
|
|
// Send the new buddy storage values
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
var query = iq.setQuery(NS_PRIVATE);
|
|
var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_ROSTERNOTES}));
|
|
|
|
// We regenerate the XML
|
|
for(var i = 0; i < storageDB.length; i++) {
|
|
// Get the pointer values
|
|
var current = storageDB.key(i);
|
|
|
|
// If the pointer is on a stored rosternote
|
|
if(explodeThis('_', current, 0) == 'rosternotes') {
|
|
var xid = explodeThis('_', current, 1);
|
|
var value = storageDB.getItem(current);
|
|
|
|
if(xid && value)
|
|
storage.appendChild(iq.buildNode('note', {'jid': xid, 'xmlns': NS_ROSTERNOTES}, value));
|
|
}
|
|
}
|
|
|
|
con.send(iq);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Switches the user-infos tabs
|
|
function switchUInfos(id) {
|
|
$('#userinfos .content .one-lap').hide();
|
|
$('#userinfos .content .info' + id).show();
|
|
$('#userinfos .tab a').removeClass('tab-active');
|
|
$('#userinfos .tab a[data-key="' + id + '"]').addClass('tab-active');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Gets the user's informations when creating a new chat
|
|
function getUserInfos(hash, xid, nick, type) {
|
|
// This is a normal chat
|
|
if(type != 'private') {
|
|
// Display the buddy name
|
|
if(nick) {
|
|
$('#' + hash + ' .top .name .bc-name').text(nick);
|
|
$('#page-switch .' + hash + ' .name').text(nick);
|
|
}
|
|
|
|
// Get the buddy PEP informations
|
|
displayAllPEP(xid);
|
|
}
|
|
|
|
// Display the buddy presence
|
|
presenceFunnel(xid, hash);
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchUserInfos() {
|
|
// Click events
|
|
$('#userinfos .tab a').click(function() {
|
|
// Yet active?
|
|
if($(this).hasClass('tab-active'))
|
|
return false;
|
|
|
|
// Switch to the good tab
|
|
var key = parseInt($(this).attr('data-key'));
|
|
|
|
return switchUInfos(key);
|
|
});
|
|
|
|
$('#userinfos .bottom .finish').click(function() {
|
|
return closeUserInfos();
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the seach tools JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 19/03/11
|
|
|
|
*/
|
|
|
|
// Searches in the user's buddy list
|
|
function processBuddySearch(query) {
|
|
// No query submitted?
|
|
if(!query)
|
|
return;
|
|
|
|
// Wildcard (*) submitted
|
|
if(query == '*')
|
|
query = '';
|
|
|
|
// Replace forbidden characters in regex
|
|
query = escapeRegex(query);
|
|
|
|
// Create an empty array
|
|
var results = new Array();
|
|
|
|
// Search regex
|
|
var regex = new RegExp('((^)|( ))' + query, 'gi');
|
|
|
|
// Search in the roster
|
|
var buddies = getAllBuddies();
|
|
|
|
for(i in buddies) {
|
|
var xid = buddies[i];
|
|
var nick = getBuddyName(xid);
|
|
|
|
// Buddy match our search, and not yet in the array
|
|
if(nick.match(regex) && !existArrayValue(results, xid))
|
|
results.push(xid);
|
|
}
|
|
|
|
// Return the results array
|
|
return results;
|
|
}
|
|
|
|
// Resets the buddy search tool
|
|
function resetBuddySearch(destination) {
|
|
$(destination + ' ul').remove();
|
|
$(destination + ' input').removeClass('suggested');
|
|
}
|
|
|
|
// Add the clicked XID to the input
|
|
function addBuddySearch(destination, xid) {
|
|
// Remove the search tool
|
|
resetBuddySearch(destination);
|
|
|
|
// Define a selector
|
|
var input = $(destination + ' input');
|
|
var value = input.val();
|
|
|
|
// Get the old value (if there's another value)
|
|
var old = '';
|
|
|
|
if(value.match(/(^(.+)(,)(\s)?)(\w+)$/))
|
|
old = RegExp.$1;
|
|
|
|
// Add the XID to the "to" input and focus on it
|
|
$(document).oneTime(10, function() {
|
|
input.val(old + xid).focus();
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
// Creates the appropriate markup for the search results
|
|
function createBuddySearch(destination) {
|
|
// Reset the search engine
|
|
resetBuddySearch(destination);
|
|
|
|
// Get the entered value
|
|
var value = $(destination + ' input').val();
|
|
|
|
// Separation with a comma?
|
|
if(value.match(/^(.+)((,)(\s)?)(\w+)$/))
|
|
value = RegExp.$5;
|
|
|
|
// Get the result array
|
|
var entered = processBuddySearch(value);
|
|
|
|
// Display each result (if any)
|
|
if(entered && entered.length) {
|
|
// Set a special class to the search input
|
|
$(destination + ' input').addClass('suggested');
|
|
|
|
// Append each found buddy in the container
|
|
var regex = new RegExp('((^)|( ))' + value, 'gi');
|
|
|
|
// Initialize the code generation
|
|
var code = '<ul>';
|
|
|
|
for(b in entered) {
|
|
// Get some values from the XID
|
|
var current = getBuddyName(entered[b]).htmlEnc();
|
|
current = current.replace(regex, '<b>$&</b>');
|
|
|
|
// Add the current element to the global code
|
|
code += '<li onclick="return addBuddySearch(\'' + encodeOnclick(destination) + '\', \'' + encodeOnclick(entered[b]) + '\');" data-xid="' + encodeQuotes(entered[b]) + '">' + current + '</li>';
|
|
}
|
|
|
|
// Finish the code generation
|
|
code += '</ul>';
|
|
|
|
// Creates the code in the DOM
|
|
$(destination).append(code);
|
|
|
|
// Put the hover on the first element
|
|
$(destination + ' ul li:first').addClass('hovered');
|
|
|
|
// Hover event, to not to remove this onblur and loose the click event
|
|
$(destination + ' ul li').hover(function() {
|
|
$(destination + ' ul li').removeClass('hovered');
|
|
$(this).addClass('hovered');
|
|
|
|
// Add a marker for the blur event
|
|
$(destination + ' ul').attr('mouse-hover', 'true');
|
|
}, function() {
|
|
$(this).removeClass('hovered');
|
|
|
|
// Remove the mouse over marker
|
|
$(destination + ' ul').removeAttr('mouse-hover');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handles the keyboard arrows press when searching
|
|
function arrowsBuddySearch(e, destination) {
|
|
// Down arrow: 40
|
|
// Up arrown: 38
|
|
|
|
// Initialize
|
|
var code = e.keyCode;
|
|
|
|
// Not the key we want here
|
|
if((code != 40) && (code != 38))
|
|
return;
|
|
|
|
// Remove the eventual mouse hover marker
|
|
$(destination + ' ul').removeAttr('mouse-hover');
|
|
|
|
// Create the path & get its size
|
|
var path = destination + ' ul li';
|
|
var pSize = $(path).size();
|
|
|
|
// Define the i value
|
|
var i = 0;
|
|
|
|
// Switching yet launched
|
|
if(exists(path + '.hovered')) {
|
|
var index = $(path).attr('data-hovered');
|
|
|
|
if(index)
|
|
i = parseInt(index);
|
|
|
|
if(code == 40)
|
|
i++;
|
|
else
|
|
i--;
|
|
}
|
|
|
|
else if(code == 38)
|
|
i = pSize - 1;
|
|
|
|
// We must not override the maximum i limit
|
|
if(i >= pSize)
|
|
i = 0;
|
|
|
|
// We must not have negative i
|
|
else if(i < 0)
|
|
i = pSize - 1;
|
|
|
|
// Modify the list
|
|
$(path + '.hovered').removeClass('hovered');
|
|
$(path).eq(i).addClass('hovered');
|
|
|
|
// Store the i index
|
|
$(path).attr('data-hovered', i);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Filters the buddies in the roster
|
|
var SEARCH_FILTERED = false;
|
|
|
|
function goFilterBuddySearch(vFilter) {
|
|
// Put a marker
|
|
SEARCH_FILTERED = true;
|
|
|
|
// Show the buddies that match the search string
|
|
var rFilter = processBuddySearch(vFilter);
|
|
|
|
// Hide all the buddies
|
|
$('#buddy-list .buddy').hide();
|
|
|
|
// Only show the buddies which match the search
|
|
if(!BLIST_ALL) {
|
|
for(i in rFilter)
|
|
$('#buddy-list .buddy[data-xid="' + escape(rFilter[i]) + '"]:not(.hidden-buddy)').show();
|
|
} else {
|
|
for(i in rFilter)
|
|
$('#buddy-list .buddy[data-xid="' + escape(rFilter[i]) + '"]').show();
|
|
}
|
|
}
|
|
|
|
// Resets the buddy filtering in the roster
|
|
function resetFilterBuddySearch() {
|
|
// Remove the marker
|
|
SEARCH_FILTERED = false;
|
|
|
|
// Show all the buddies
|
|
$('#buddy-list .buddy').show();
|
|
|
|
// Only show available buddies
|
|
if(!BLIST_ALL)
|
|
$('#buddy-list .buddy.hidden-buddy').hide();
|
|
|
|
// Update the groups
|
|
updateGroups();
|
|
}
|
|
|
|
// Funnels the buddy filtering
|
|
function funnelFilterBuddySearch(keycode) {
|
|
// Get the input value
|
|
var input = $('#buddy-list .filter input');
|
|
var cancel = $('#buddy-list .filter a');
|
|
var value = input.val();
|
|
|
|
// Security: reset all the groups, empty or not, deployed or not
|
|
$('#buddy-list .one-group, #buddy-list .group-buddies').show();
|
|
$('#buddy-list .group span').text('-');
|
|
|
|
// Nothing is entered, or escape pressed
|
|
if(!value || (keycode == 27)) {
|
|
if(keycode == 27)
|
|
input.val('');
|
|
|
|
resetFilterBuddySearch();
|
|
cancel.hide();
|
|
}
|
|
|
|
// Process the filtering
|
|
else {
|
|
cancel.show();
|
|
goFilterBuddySearch(value);
|
|
}
|
|
|
|
// Update the groups
|
|
updateGroups();
|
|
}
|
|
|
|
// Searches for the nearest element (with a lower stamp than the current one)
|
|
function sortElementByStamp(stamp, element) {
|
|
var array = new Array();
|
|
var i = 0;
|
|
var nearest = 0;
|
|
|
|
// Add the stamp values to the array
|
|
$(element).each(function() {
|
|
var current_stamp = parseInt($(this).attr('data-stamp'));
|
|
|
|
// Push it!
|
|
array.push(current_stamp);
|
|
});
|
|
|
|
// Sort the array
|
|
array.sort();
|
|
|
|
// Get the nearest stamp value
|
|
while(stamp > array[i]) {
|
|
nearest = array[i];
|
|
|
|
i++;
|
|
}
|
|
|
|
return nearest;
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the autocompletion tools JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 12/11/10
|
|
|
|
*/
|
|
|
|
// Sort an array with insensitivity to the case
|
|
function caseInsensitiveSort(a, b) {
|
|
// Put the two strings into lower case
|
|
a = a.toLowerCase();
|
|
b = b.toLowerCase();
|
|
|
|
// Process the sort
|
|
if(a > b)
|
|
return 1;
|
|
if(a < b)
|
|
return -1;
|
|
}
|
|
|
|
// Creates an array with the autocompletion results
|
|
function processAutocompletion(query, id) {
|
|
// Replace forbidden characters in regex
|
|
query = escapeRegex(query);
|
|
|
|
// Create an empty array
|
|
var results = new Array();
|
|
|
|
// Search in the roster
|
|
$('#' + id + ' .user').each(function() {
|
|
var nick = $(this).find('.name').text();
|
|
var regex = new RegExp('(^)' + query, 'gi');
|
|
|
|
if(nick.match(regex))
|
|
results.push(nick);
|
|
});
|
|
|
|
// Sort the array
|
|
results = results.sort(caseInsensitiveSort);
|
|
|
|
// Return the results array
|
|
return results;
|
|
}
|
|
|
|
// Resets the autocompletion tools
|
|
function resetAutocompletion(hash) {
|
|
$('#' + hash + ' .message-area').removeAttr('data-autocompletion-pointer').removeAttr('data-autocompletion-query');
|
|
}
|
|
|
|
// Autocompletes the chat input nick
|
|
function createAutocompletion(hash) {
|
|
// Initialize
|
|
var vSelector = $('#' + hash + ' .message-area');
|
|
var value = vSelector.val();
|
|
if(!value)
|
|
resetAutocompletion(hash);
|
|
var query = vSelector.attr('data-autocompletion-query');
|
|
|
|
// The autocompletion has not been yet launched
|
|
if(query == undefined) {
|
|
query = value;
|
|
vSelector.attr('data-autocompletion-query', query);
|
|
}
|
|
|
|
// Get the pointer
|
|
var pointer = vSelector.attr('data-autocompletion-pointer');
|
|
var i = 0;
|
|
|
|
if(pointer)
|
|
i = parseInt(pointer);
|
|
|
|
// We get the nickname
|
|
var nick = processAutocompletion(query, hash)[i];
|
|
|
|
// Shit, this is my nick!
|
|
if((nick != undefined) && (nick.toLowerCase() == getMUCNick(hash).toLowerCase())) {
|
|
// Increment
|
|
i++;
|
|
|
|
// Get the next nick
|
|
nick = processAutocompletion(query, hash)[i];
|
|
}
|
|
|
|
// We quote the nick
|
|
if(nick != undefined) {
|
|
// Increment
|
|
i++;
|
|
quoteMyNick(hash, nick);
|
|
|
|
// Put a pointer
|
|
vSelector.attr('data-autocompletion-pointer', i);
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the welcome tool functions for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 16/01/12
|
|
|
|
*/
|
|
|
|
// Opens the welcome tools
|
|
function openWelcome() {
|
|
// Share message
|
|
var share_msg = printf(_e("Using Jappix, an open social platform. I am %s!"), getXID());
|
|
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Welcome!") + '</div>' +
|
|
|
|
'<div class="tab">' +
|
|
'<a href="#" class="tab-active" data-step="1">' + _e("Options") + '</a>' +
|
|
'<a href="#" class="tab-missing" data-step="2">' + _e("Friends") + '</a>' +
|
|
'<a href="#" class="tab-missing" data-step="3">' + _e("Share") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="lap-active one-lap welcome1">' +
|
|
'<div class="infos">' +
|
|
'<p class="infos-title">' + _e("Welcome on Jappix, your own social cloud!") + '</p>' +
|
|
'<p>' + _e("Before you start using it, you will have to change some settings, search for friends and complete your profile.") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<a href="#" class="box enabled" title="' + _e("Click to disable") + '">' +
|
|
'<span class="option">' + _e("Sounds") + '</span>' +
|
|
'<span class="description">' + _e("Enable notification sounds") + '</span>' +
|
|
'<span class="image sound talk-images"></span>' +
|
|
'<span class="tick talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a href="#" class="box enabled pep-hidable" title="' + _e("Click to disable") + '">' +
|
|
'<span class="option">' + _e("Geolocation") + '</span>' +
|
|
'<span class="description">' + _e("Share your position on the globe") + '</span>' +
|
|
'<span class="image geolocation talk-images"></span>' +
|
|
'<span class="tick talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a href="#" class="box xmpplinks-hidable" title="' + _e("Click to enable") + '">' +
|
|
'<span class="option">' + _e("XMPP links") + '</span>' +
|
|
'<span class="description">' + _e("Open XMPP links with Jappix") + '</span>' +
|
|
'<span class="image xmpp talk-images"></span>' +
|
|
'<span class="tick talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a href="#" class="box mam-hidable pref" title="' + _e("Click to enable") + '">' +
|
|
'<span class="option">' + _e("Message archiving") + '</span>' +
|
|
'<span class="description">' + _e("Store a history of your chats") + '</span>' +
|
|
'<span class="image mam talk-images"></span>' +
|
|
'<span class="tick talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a href="#" class="box" title="' + _e("Click to enable") + '">' +
|
|
'<span class="option">' + _e("Offline friends") + '</span>' +
|
|
'<span class="description">' + _e("Don\'t hide offline friends") + '</span>' +
|
|
'<span class="image offline talk-images"></span>' +
|
|
'<span class="tick talk-images"></span>' +
|
|
'</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="one-lap welcome2">' +
|
|
'<div class="infos">' +
|
|
'<p class="infos-title">' + _e("Friends") + '</p>' +
|
|
'<p>' + _e("Use this tool to find your friends on the server you are using right now, or add them later.") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<div class="results welcome-results"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="one-lap welcome3">' +
|
|
'<div class="infos">' +
|
|
'<p class="infos-title">' + _e("Share") + '</p>' +
|
|
'<p>' + _e("Good job! Now, you can share Jappix with your friends!") + '</p>' +
|
|
'<p>' + _e("When you will press the save button, the profile editor will be opened. Happy socializing!") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<a class="box share first" href="http://www.facebook.com/sharer/sharer.php?u=' + encodeQuotes(generateURL(JAPPIX_LOCATION)) + '" target="_blank">' +
|
|
'<span class="logo facebook welcome-images"></span>' +
|
|
'<span class="name">Facebook</span>' +
|
|
'<span class="description">' + printf(_e("Share Jappix on %s"), 'Facebook') + '</span>' +
|
|
'<span class="go talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a class="box share" href="http://twitter.com/intent/tweet?text=' + encodeQuotes(share_msg) + '&url=' + encodeQuotes(generateURL(JAPPIX_LOCATION)) + '" target="_blank">' +
|
|
'<span class="logo twitter welcome-images"></span>' +
|
|
'<span class="name">Twitter</span>' +
|
|
'<span class="description">' + printf(_e("Share Jappix on %s"), 'Twitter') + '</span>' +
|
|
'<span class="go talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a class="box share" href="http://www.google.com/buzz/post?message=' + encodeQuotes(share_msg) + '&url=' + encodeQuotes(generateURL(JAPPIX_LOCATION)) + '" target="_blank">' +
|
|
'<span class="logo buzz welcome-images"></span>' +
|
|
'<span class="name">Google Buzz</span>' +
|
|
'<span class="description">' + printf(_e("Share Jappix on %s"), 'Google Buzz') + '</span>' +
|
|
'<span class="go talk-images"></span>' +
|
|
'</a>' +
|
|
|
|
'<a class="box share" href="http://identi.ca/index.php?action=newnotice&status_textarea=' + encodeQuotes(share_msg) + ' ' + encodeQuotes(generateURL(JAPPIX_LOCATION)) + '" target="_blank">' +
|
|
'<span class="logo identica welcome-images"></span>' +
|
|
'<span class="name">Identi.ca</span>' +
|
|
'<span class="description">' + printf(_e("Share Jappix on %s"), 'Identi.ca') + '</span>' +
|
|
'<span class="go talk-images"></span>' +
|
|
'</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<div class="wait wait-medium"></div>' +
|
|
|
|
'<a href="#" class="finish next">' + _e("Next") + ' »</a>' +
|
|
'<a href="#" class="finish save">' + _e("Save") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('welcome', html);
|
|
|
|
// Unavoidable popup
|
|
$('#welcome').addClass('unavoidable');
|
|
|
|
// Apply the features
|
|
applyFeatures('welcome');
|
|
|
|
// Associate the events
|
|
launchWelcome();
|
|
|
|
logThis('Welcome assistant opened.');
|
|
}
|
|
|
|
// Closes the welcome tools
|
|
function closeWelcome() {
|
|
// Destroy the popup
|
|
destroyPopup('welcome');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Switches the welcome tabs
|
|
function switchWelcome(id) {
|
|
// Path to
|
|
var welcome = '#welcome ';
|
|
var content = welcome + '.content .';
|
|
var tab = welcome + '.tab ';
|
|
var wait = $(welcome + '.wait');
|
|
|
|
$(content + 'one-lap').hide();
|
|
$(content + 'welcome' + id).show();
|
|
$(tab + 'a').removeClass('tab-active');
|
|
$(tab + 'a[data-step="' + id + '"]').addClass('tab-active').removeClass('tab-missing');
|
|
|
|
// Update the "save" button if all is okay
|
|
if(!exists(tab + '.tab-missing')) {
|
|
var finish = welcome + '.finish.';
|
|
$(finish + 'save').show();
|
|
$(finish + 'next').hide();
|
|
}
|
|
|
|
// If this is ID 2: vJUD search
|
|
if(id == 2) {
|
|
wait.show();
|
|
dataForm(HOST_VJUD, 'search', '', '', 'welcome');
|
|
}
|
|
|
|
else
|
|
wait.hide();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Sends the welcome options
|
|
function sendWelcome(array) {
|
|
// Sends the options
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var query = iq.setQuery(NS_PRIVATE);
|
|
var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_OPTIONS}));
|
|
|
|
// Value array
|
|
var tags = new Array('sounds', 'geolocation', '', '', 'roster-showall');
|
|
|
|
// Build the XML with the array
|
|
for(i in array) {
|
|
var value = array[i];
|
|
var tag = tags[i];
|
|
|
|
if((i != 2) && (i != 3) && tag && value) {
|
|
storage.appendChild(iq.buildNode('option', {'type': tag, 'xmlns': NS_OPTIONS}, value));
|
|
setDB('options', tag, value);
|
|
}
|
|
}
|
|
|
|
con.send(iq);
|
|
|
|
// If geolocation is enabled
|
|
if(array[1] == '1')
|
|
geolocate();
|
|
}
|
|
|
|
// Saves the welcome options
|
|
var END_WELCOME = false;
|
|
|
|
function saveWelcome() {
|
|
// Get the new options
|
|
var array = new Array();
|
|
|
|
$('#welcome a.box').each(function() {
|
|
var current = '0';
|
|
|
|
if($(this).hasClass('enabled'))
|
|
current = '1';
|
|
|
|
array.push(current);
|
|
});
|
|
|
|
// If XMPP links is enabled
|
|
if(array[2] == '1')
|
|
xmppLinksHandler();
|
|
|
|
// If offline buddies showing is enabled
|
|
if(array[4] == '1')
|
|
showAllBuddies('welcome');
|
|
|
|
// If archiving is supported by the server
|
|
if(enabledMAM()) {
|
|
// If archiving is enabled
|
|
if(array[3] == '1') {
|
|
setConfigMAM('roster');
|
|
}
|
|
}
|
|
|
|
// Send the new options
|
|
sendWelcome(array);
|
|
|
|
// Close the welcome tool
|
|
closeWelcome();
|
|
|
|
// Open the profile editor
|
|
openVCard();
|
|
|
|
// Unavoidable popup
|
|
$('#vcard').addClass('unavoidable');
|
|
|
|
END_WELCOME = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Goes to the next welcome step
|
|
function nextWelcome() {
|
|
// Check the next step to go to
|
|
var next = 1;
|
|
var missing = '#welcome .tab a.tab-missing';
|
|
|
|
if(exists(missing))
|
|
next = parseInt($(missing + ':first').attr('data-step'));
|
|
|
|
// Switch to the next step
|
|
switchWelcome(next);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchWelcome() {
|
|
// Click events
|
|
$('#welcome .tab a').click(function() {
|
|
// Switch to the good tab
|
|
var key = parseInt($(this).attr('data-step'));
|
|
|
|
return switchWelcome(key);
|
|
});
|
|
|
|
$('#welcome a.box:not(.share)').click(function() {
|
|
if($(this).hasClass('enabled'))
|
|
$(this).removeClass('enabled').attr('title', _e("Click to enable"));
|
|
else
|
|
$(this).addClass('enabled').attr('title', _e("Click to disable"));
|
|
|
|
return false;
|
|
});
|
|
|
|
$('#welcome .bottom .finish').click(function() {
|
|
if($(this).is('.next'))
|
|
return nextWelcome();
|
|
if($(this).is('.save'))
|
|
return saveWelcome();
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the Jappix Me tool functions for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 21/06/12
|
|
|
|
*/
|
|
|
|
// Opens the Me tools
|
|
function openMe() {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Public profile") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<a class="me-images logo" href="https://me.jappix.com/" target="_blank"></a>' +
|
|
|
|
'<div class="infos">' +
|
|
'<p class="infos-title">' + _e("Your profile anywhere on the Web.") + '</p>' +
|
|
'<p>' + printf(_e("%s is a Jappix.com service which makes your XMPP profile public. It is easier to share it. No XMPP account is required to view your social channel, your current position and your contact details."), '<a href="https://me.jappix.com/" target="_blank">Jappix Me</a>') + '</p>' +
|
|
'<p>' + _e("Furthermore, every picture you post in your social channel is added to a beautiful picture timeline. You can now view the pictures you shared year by year.") + '</p>' +
|
|
'<p>' + _e("You can also use your XMPP avatar as a single avatar for every website, blog and forum you use. When you change it on XMPP, the new avatar appears everywhere. What a genius improvement!") + '</p>' +
|
|
'</div>' +
|
|
|
|
'<a class="go one-button" href="https://me.jappix.com/new" target="_blank">' + _e("Yay, let's create your public profile!") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<a href="#" class="finish">' + _e("Close") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('me', html);
|
|
|
|
// Associate the events
|
|
launchMe();
|
|
|
|
logThis('Public profile tool opened.');
|
|
}
|
|
|
|
// Closes the Me tools
|
|
function closeMe() {
|
|
// Destroy the popup
|
|
destroyPopup('me');
|
|
|
|
// We finished
|
|
END_WELCOME = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchMe() {
|
|
// Click events
|
|
$('#me .content a.go').click(function() {
|
|
closeMe();
|
|
});
|
|
|
|
$('#me .bottom .finish').click(closeMe);
|
|
}
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
These are the Roster Item Exchange JS script for Jappix
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 20/06/12
|
|
|
|
*/
|
|
|
|
// Opens the rosterx tools
|
|
function openRosterX(data) {
|
|
// Popup HTML content
|
|
var html =
|
|
'<div class="top">' + _e("Suggested friends") + '</div>' +
|
|
|
|
'<div class="content">' +
|
|
'<div class="rosterx-head">' +
|
|
'<a href="#" class="uncheck">' + _e("Uncheck all") + '</a>' +
|
|
'<a href="#" class="check">' + _e("Check all") + '</a>' +
|
|
'</div>' +
|
|
|
|
'<div class="results"></div>' +
|
|
'</div>' +
|
|
|
|
'<div class="bottom">' +
|
|
'<a href="#" class="finish save">' + _e("Save") + '</a>' +
|
|
'<a href="#" class="finish cancel">' + _e("Cancel") + '</a>' +
|
|
'</div>';
|
|
|
|
// Create the popup
|
|
createPopup('rosterx', html);
|
|
|
|
// Associate the events
|
|
launchRosterX();
|
|
|
|
// Parse the data
|
|
parseRosterX(data);
|
|
|
|
logThis('Roster Item Exchange popup opened.');
|
|
}
|
|
|
|
// Closes the rosterx tools
|
|
function closeRosterX() {
|
|
// Destroy the popup
|
|
destroyPopup('rosterx');
|
|
|
|
return false;
|
|
}
|
|
|
|
// Parses a rosterx query
|
|
function parseRosterX(data) {
|
|
// Main selector
|
|
var x = $(data).find('x[xmlns="' + NS_ROSTERX + '"]:first');
|
|
|
|
// Parse data
|
|
x.find('item').each(function() {
|
|
// Generate group XML
|
|
var group = '';
|
|
|
|
$(this).find('group').each(function() {
|
|
group += '<group>' + $(this).text().htmlEnc() + '</group>';
|
|
});
|
|
|
|
if(group)
|
|
group = '<groups>' + group + '</groups>';
|
|
|
|
// Display it!
|
|
displayRosterX($(this).attr('jid'), $(this).attr('name'), group, $(this).attr('action'));
|
|
});
|
|
|
|
// Click to check/uncheck
|
|
$('#rosterx .oneresult').click(function(evt) {
|
|
// No need to apply when click on input
|
|
if($(evt.target).is('input[type="checkbox"]'))
|
|
return;
|
|
|
|
// Input selector
|
|
var checkbox = $(this).find('input[type="checkbox"]');
|
|
|
|
// Check or uncheck?
|
|
if(checkbox.filter(':checked').size())
|
|
checkbox.removeAttr('checked');
|
|
else
|
|
checkbox.attr('checked', true);
|
|
});
|
|
}
|
|
|
|
// Displays a rosterx item
|
|
function displayRosterX(xid, nick, group, action) {
|
|
// End if no XID
|
|
if(!xid)
|
|
return false;
|
|
|
|
// Set up a default action if no one
|
|
if(!action || (action != 'modify') || (action != 'delete'))
|
|
action = 'add';
|
|
|
|
// Override "undefined" for nickname
|
|
if(!nick)
|
|
nick = '';
|
|
|
|
// Display it
|
|
$('#rosterx .results').append(
|
|
'<div class="oneresult">' +
|
|
'<input type="checkbox" checked="" data-name="' + encodeQuotes(nick) + '" data-xid="' + encodeQuotes(xid) + '" data-action="' + encodeQuotes(action) + '" data-group="' + encodeQuotes(group) + '" />' +
|
|
'<span class="name">' + nick.htmlEnc() + '</span>' +
|
|
'<span class="xid">' + xid.htmlEnc() + '</span>' +
|
|
'<span class="action ' + action + ' talk-images"></span>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
|
|
// Saves the rosterx settings
|
|
function saveRosterX() {
|
|
// Send the requests
|
|
$('#rosterx .results input[type="checkbox"]').filter(':checked').each(function() {
|
|
// Read the attributes
|
|
var nick = $(this).attr('data-name');
|
|
var xid = $(this).attr('data-xid');
|
|
var action = $(this).attr('data-action');
|
|
var group = $(this).attr('data-group');
|
|
|
|
// Parse groups XML
|
|
if(group) {
|
|
var group_arr = []
|
|
|
|
$(group).find('group').each(function() {
|
|
group_arr.push($(this).text().revertHtmlEnc());
|
|
});
|
|
}
|
|
|
|
// Process the asked action
|
|
var roster_item = $('#buddy-list .' + hex_md5(xid));
|
|
|
|
switch(action) {
|
|
// Buddy add
|
|
case 'add':
|
|
if(!exists(roster_item)) {
|
|
sendSubscribe(xid, 'subscribe');
|
|
sendRoster(xid, '', nick, group_arr);
|
|
}
|
|
|
|
break;
|
|
|
|
// Buddy edit
|
|
case 'modify':
|
|
if(exists(roster_item))
|
|
sendRoster(xid, '', nick, group_arr);
|
|
|
|
break;
|
|
|
|
// Buddy delete
|
|
case 'delete':
|
|
if(exists(roster_item))
|
|
sendRoster(xid, 'remove');
|
|
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Close the popup
|
|
closeRosterX();
|
|
}
|
|
|
|
// Plugin launcher
|
|
function launchRosterX() {
|
|
// Click events
|
|
$('#rosterx .bottom .finish').click(function() {
|
|
if($(this).is('.save'))
|
|
return saveRosterX();
|
|
if($(this).is('.cancel'))
|
|
return closeRosterX();
|
|
});
|
|
|
|
$('#rosterx .rosterx-head a').click(function() {
|
|
if($(this).is('.check'))
|
|
$('#rosterx .results input[type="checkbox"]').attr('checked', true);
|
|
else if($(this).is('.uncheck'))
|
|
$('#rosterx .results input[type="checkbox"]').removeAttr('checked');
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/*
|
|
|
|
Jappix - An open social platform
|
|
Implementation of XEP-0313: Message Archive Management
|
|
|
|
-------------------------------------------------
|
|
|
|
License: AGPL
|
|
Author: Valérian Saliou
|
|
Last revision: 04/08/13
|
|
|
|
*/
|
|
|
|
|
|
/* -- MAM Constants -- */
|
|
// Note: Internet Explorer does not support 'const'
|
|
// We use vars as a fix...
|
|
var MAM_REQ_MAX = 25;
|
|
|
|
var MAM_PREF_DEFAULTS = {
|
|
'always' : 1,
|
|
'never' : 1,
|
|
'roster' : 1
|
|
};
|
|
|
|
|
|
/* -- MAM Variables -- */
|
|
var MAM_MAP_REQS = {};
|
|
var MAM_MAP_PENDING = {};
|
|
var MAM_MAP_STATES = {};
|
|
var MAM_MSG_QUEUE = {};
|
|
|
|
|
|
/* -- MAM Configuration -- */
|
|
|
|
// Gets the MAM configuration
|
|
function getConfigMAM() {
|
|
try {
|
|
// Lock the archiving options
|
|
$('#archiving').attr('disabled', true);
|
|
|
|
// Get the archiving configuration
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
|
|
iq.appendNode('prefs', { 'xmlns': NS_URN_MAM });
|
|
|
|
con.send(iq, handleConfigMAM);
|
|
} catch(e) {
|
|
logThis('getConfigMAM > ' + e, 1);
|
|
}
|
|
}
|
|
|
|
// Handles the MAM configuration
|
|
function handleConfigMAM(iq) {
|
|
try {
|
|
if(iq.getType() != 'error') {
|
|
// Read packet
|
|
var cur_default = $(iq.getNode()).find('prefs[xmlns="' + NS_URN_MAM + '"]').attr('default') || 'never';
|
|
|
|
if(!(cur_default in MAM_PREF_DEFAULTS)) {
|
|
cur_default = 'never';
|
|
}
|
|
|
|
// Apply value to options
|
|
$('#archiving').val(cur_default);
|
|
}
|
|
|
|
// Unlock the archiving option
|
|
$('#archiving').removeAttr('disabled');
|
|
|
|
// All done.
|
|
waitOptions('mam');
|
|
} catch(e) {
|
|
logThis('handleConfigMAM > ' + e, 1);
|
|
}
|
|
}
|
|
|
|
// Sets the MAM configuration
|
|
function setConfigMAM(pref_default) {
|
|
try {
|
|
// Check parameters
|
|
if(!(pref_default in MAM_PREF_DEFAULTS)) {
|
|
pref_default = 'never';
|
|
}
|
|
|
|
// Send new configuration
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
iq.appendNode('prefs', { 'xmlns': NS_URN_MAM, 'default': pref_default });
|
|
|
|
con.send(iq);
|
|
} catch(e) {
|
|
logThis('setConfigMAM > ' + e, 1);
|
|
}
|
|
}
|
|
|
|
|
|
/* -- MAM Purge -- */
|
|
|
|
// Removes all (or given) MAM archives
|
|
function purgeArchivesMAM(args) {
|
|
try {
|
|
if(typeof args != 'object') {
|
|
args = {};
|
|
}
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('set');
|
|
|
|
var purge = iq.appendNode('purge', { 'xmlns': NS_URN_MAM });
|
|
|
|
for(c in args) {
|
|
if(args[c]) purge.appendChild(iq.buildNode(c, {'xmlns': NS_URN_MAM}, args[c]));
|
|
}
|
|
|
|
con.send(iq, function(iq) {
|
|
if(iq.getType() == 'result') {
|
|
logThis('Archives purged (MAM).', 3);
|
|
} else {
|
|
logThis('Error purging archives (MAM).', 1);
|
|
}
|
|
});
|
|
} catch(e) {
|
|
logThis('purgeArchivesMAM > ' + e, 1);
|
|
}
|
|
}
|
|
|
|
|
|
/* -- MAM Retrieval -- */
|
|
|
|
// Gets the MAM configuration
|
|
function getArchivesMAM(args, rsm_args, callback) {
|
|
try {
|
|
if(typeof args != 'object') {
|
|
args = {};
|
|
}
|
|
|
|
var req_id = genID();
|
|
|
|
if(args['with']) {
|
|
MAM_MAP_PENDING[args['with']] = 1;
|
|
MAM_MAP_REQS[req_id] = args['with'];
|
|
}
|
|
|
|
var iq = new JSJaCIQ();
|
|
iq.setType('get');
|
|
iq.setID(req_id);
|
|
|
|
var query = iq.setQuery(NS_URN_MAM);
|
|
|
|
for(c in args) {
|
|
if(args[c] != null) query.appendChild(iq.buildNode(c, {'xmlns': NS_URN_MAM}, args[c]));
|
|
}
|
|
|
|
if(rsm_args && typeof rsm_args == 'object') {
|
|
var rsm_set = query.appendChild(iq.buildNode('set', {'xmlns': NS_RSM}));
|
|
|
|
for(r in rsm_args) {
|
|
if(rsm_args[r] != null) rsm_set.appendChild(iq.buildNode(r, {'xmlns': NS_RSM}, rsm_args[r]));
|
|
}
|
|
}
|
|
|
|
con.send(iq, function(res_iq) {
|
|
handleArchivesMAM(res_iq, callback);
|
|
});
|
|
} catch(e) {
|
|
logThis('getArchivesMAM > ' + e, 1);
|
|
}
|
|
}
|
|
|
|
// Handles the MAM configuration
|
|
function handleArchivesMAM(iq, callback) {
|
|
try {
|
|
var res_id = iq.getID();
|
|
var res_with;
|
|
|
|
if(res_id && res_id in MAM_MAP_REQS) {
|
|
res_with = MAM_MAP_REQS[res_id];
|
|
}
|
|
|
|
if(iq.getType() != 'error') {
|
|
if(res_with) {
|
|
var res_sel = $(iq.getQuery());
|
|
var res_rsm_sel = res_sel.find('set[xmlns="' + NS_RSM + '"]');
|
|
|
|
// Store that data
|
|
MAM_MAP_STATES[res_with] = {
|
|
'date': {
|
|
'start': res_sel.find('start').eq(0).text(),
|
|
'end': res_sel.find('end').eq(0).text()
|
|
},
|
|
|
|
'rsm': {
|
|
'first': res_rsm_sel.find('first').eq(0).text(),
|
|
'last': res_rsm_sel.find('last').eq(0).text(),
|
|
'count': parseInt(res_rsm_sel.find('count').eq(0).text() || 0)
|
|
}
|
|
}
|
|
|
|
// Generate stamps for easy operations
|
|
var start_stamp = extractStamp(Date.jab2date(MAM_MAP_STATES[res_with]['date']['start']));
|
|
var start_end = extractStamp(Date.jab2date(MAM_MAP_STATES[res_with]['date']['end']));
|
|
|
|
// Create MAM messages target
|
|
var target_html = '<div class="mam-chunk" data-start="' + encodeQuotes(start_stamp) + '" data-end="' + encodeQuotes(start_end) + '"></div>';
|
|
|
|
var target_content_sel = $('#' + hex_md5(res_with) + ' .content');
|
|
var target_wait_sel = target_content_sel.find('.wait-mam');
|
|
|
|
if(target_wait_sel.size()) {
|
|
target_wait_sel.after(target_html);
|
|
} else {
|
|
target_content_sel.prepend(target_html);
|
|
}
|
|
|
|
// Any enqueued message to display?
|
|
if(typeof MAM_MSG_QUEUE[res_with] == 'object') {
|
|
for(i in MAM_MSG_QUEUE[res_with]) {
|
|
(MAM_MSG_QUEUE[res_with][i])();
|
|
}
|
|
|
|
delete MAM_MSG_QUEUE[res_with];
|
|
}
|
|
|
|
// Remove XID from pending list
|
|
if(res_with in MAM_MAP_PENDING) {
|
|
delete MAM_MAP_PENDING[res_with];
|
|
}
|
|
|
|
logThis('Got archives from: ' + res_with, 3);
|
|
} else {
|
|
logThis('Could not associate archive response with a known JID.', 2);
|
|
}
|
|
} else {
|
|
logThis('Error handing archives (MAM).', 1);
|
|
}
|
|
|
|
// Execute callback?
|
|
if(typeof callback == 'function') {
|
|
callback(iq);
|
|
}
|
|
} catch(e) {
|
|
logThis('handleArchivesMAM > ' + e, 1);
|
|
}
|
|
}
|
|
|
|
// Handles a MAM-forwarded message stanza
|
|
function handleMessageMAM(fwd_stanza, c_delay) {
|
|
try {
|
|
// Build message node
|
|
var c_message = fwd_stanza.find('message');
|
|
|
|
if(c_message[0]) {
|
|
// Re-build a proper JSJaC message stanza
|
|
var message = JSJaCPacket.wrapNode(c_message[0]);
|
|
|
|
// Check message type
|
|
var type = message.getType() || 'chat';
|
|
|
|
if(type == 'chat') {
|
|
// Read message data
|
|
var xid = bareXID(getStanzaFrom(message));
|
|
var id = message.getID();
|
|
var from_xid = xid;
|
|
var b_name = getBuddyName(xid);
|
|
var mode = (xid == getXID()) ? 'me': 'him';
|
|
|
|
// Refactor chat XID (in case we were the sender of the archived message)
|
|
if(mode == 'me') {
|
|
xid = bareXID(message.getTo())
|
|
}
|
|
|
|
var hash = hex_md5(xid);
|
|
var body = message.getBody();
|
|
|
|
// Read delay (required since we deal w/ a past message!)
|
|
var time, stamp;
|
|
var delay = c_delay.attr('stamp');
|
|
|
|
if(delay) {
|
|
time = relativeDate(delay);
|
|
stamp = extractStamp(Date.jab2date(delay));
|
|
}
|
|
|
|
// Last-minute checks before display
|
|
if(time && stamp && body) {
|
|
var mam_chunk_path = '#' + hash + ' .mam-chunk';
|
|
|
|
// No chat auto-scroll?
|
|
var no_scroll = exists(mam_chunk_path);
|
|
|
|
// Select the custom target
|
|
var c_target_sel = function() {
|
|
return $(mam_chunk_path).filter(function() {
|
|
return $(this).attr('data-start') <= stamp && $(this).attr('data-end') >= stamp
|
|
}).filter(':first');
|
|
};
|
|
|
|
// Display the message in that target
|
|
var c_msg_display = function() {
|
|
displayMessage(type, from_xid, hash, b_name.htmlEnc(), body, time, stamp, 'old-message', true, null, mode, null, c_target_sel(), no_scroll);
|
|
};
|
|
|
|
// Hack: do not display the message in case we would duplicate it w/ current session messages
|
|
// only used when initiating a new chat, avoids collisions
|
|
if(!(xid in MAM_MAP_STATES) && $('#' + hash).find('.one-line.user-message:last').text() == body) {
|
|
return;
|
|
}
|
|
|
|
if(c_target_sel().size()) {
|
|
// Display the message in that target
|
|
c_msg_display();
|
|
} else {
|
|
// Delay display (we may not have received the MAM reply ATM)
|
|
if(typeof MAM_MSG_QUEUE[xid] != 'object') {
|
|
MAM_MSG_QUEUE[xid] = [];
|
|
}
|
|
|
|
MAM_MSG_QUEUE[xid].push(c_msg_display);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch(e) {
|
|
logThis('handleMessageMAM > ' + e, 1);
|
|
}
|
|
}
|