/** * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/almond for details */ //Going sloppy to avoid 'use strict' string cost, but strict practices should //be followed. /*jslint sloppy: true */ /*global setTimeout: false */ var requirejs, require, define; (function (undef) { var main, req, makeMap, handlers, defined = {}, waiting = {}, config = {}, defining = {}, hasOwn = Object.prototype.hasOwnProperty, aps = [].slice, jsSuffixRegExp = /\.js$/; function hasProp(obj, prop) { return hasOwn.call(obj, prop); } /** * Given a relative module name, like ./something, normalize it to * a real name that can be mapped to a path. * @param {String} name the relative name * @param {String} baseName a real name that the name arg is relative * to. * @returns {String} normalized name */ function normalize(name, baseName) { var nameParts, nameSegment, mapValue, foundMap, lastIndex, foundI, foundStarMap, starI, i, j, part, baseParts = baseName && baseName.split("/"), map = config.map, starMap = (map && map['*']) || {}; //Adjust any relative paths. if (name && name.charAt(0) === ".") { //If have a base name, try to normalize against it, //otherwise, assume it is a top-level require that will //be relative to baseUrl in the end. if (baseName) { name = name.split('/'); lastIndex = name.length - 1; // Node .js allowance: if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); } //Lop off the last part of baseParts, so that . matches the //"directory" and not name of the baseName's module. For instance, //baseName of "one/two/three", maps to "one/two/three.js", but we //want the directory, "one/two" for this normalization. name = baseParts.slice(0, baseParts.length - 1).concat(name); //start trimDots for (i = 0; i < name.length; i += 1) { part = name[i]; if (part === ".") { name.splice(i, 1); i -= 1; } else if (part === "..") { if (i === 1 && (name[2] === '..' || name[0] === '..')) { //End of the line. Keep at least one non-dot //path segment at the front so it can be mapped //correctly to disk. Otherwise, there is likely //no path mapping for a path starting with '..'. //This can still fail, but catches the most reasonable //uses of .. break; } else if (i > 0) { name.splice(i - 1, 2); i -= 2; } } } //end trimDots name = name.join("/"); } else if (name.indexOf('./') === 0) { // No baseName, so this is ID is resolved relative // to baseUrl, pull off the leading dot. name = name.substring(2); } } //Apply map config if available. if ((baseParts || starMap) && map) { nameParts = name.split('/'); for (i = nameParts.length; i > 0; i -= 1) { nameSegment = nameParts.slice(0, i).join("/"); if (baseParts) { //Find the longest baseName segment match in the config. //So, do joins on the biggest to smallest lengths of baseParts. for (j = baseParts.length; j > 0; j -= 1) { mapValue = map[baseParts.slice(0, j).join('/')]; //baseName segment has config, find if it has one for //this name. if (mapValue) { mapValue = mapValue[nameSegment]; if (mapValue) { //Match, update name to the new value. foundMap = mapValue; foundI = i; break; } } } } if (foundMap) { break; } //Check for a star map match, but just hold on to it, //if there is a shorter segment match later in a matching //config, then favor over this star map. if (!foundStarMap && starMap && starMap[nameSegment]) { foundStarMap = starMap[nameSegment]; starI = i; } } if (!foundMap && foundStarMap) { foundMap = foundStarMap; foundI = starI; } if (foundMap) { nameParts.splice(0, foundI, foundMap); name = nameParts.join('/'); } } return name; } function makeRequire(relName, forceSync) { return function () { //A version of a require function that passes a moduleName //value for items that may need to //look up paths relative to the moduleName var args = aps.call(arguments, 0); //If first arg is not require('string'), and there is only //one arg, it is the array form without a callback. Insert //a null so that the following concat is correct. if (typeof args[0] !== 'string' && args.length === 1) { args.push(null); } return req.apply(undef, args.concat([relName, forceSync])); }; } function makeNormalize(relName) { return function (name) { return normalize(name, relName); }; } function makeLoad(depName) { return function (value) { defined[depName] = value; }; } function callDep(name) { if (hasProp(waiting, name)) { var args = waiting[name]; delete waiting[name]; defining[name] = true; main.apply(undef, args); } if (!hasProp(defined, name) && !hasProp(defining, name)) { throw new Error('No ' + name); } return defined[name]; } //Turns a plugin!resource to [plugin, resource] //with the plugin being undefined if the name //did not have a plugin prefix. function splitPrefix(name) { var prefix, index = name ? name.indexOf('!') : -1; if (index > -1) { prefix = name.substring(0, index); name = name.substring(index + 1, name.length); } return [prefix, name]; } /** * Makes a name map, normalizing the name, and using a plugin * for normalization if necessary. Grabs a ref to plugin * too, as an optimization. */ makeMap = function (name, relName) { var plugin, parts = splitPrefix(name), prefix = parts[0]; name = parts[1]; if (prefix) { prefix = normalize(prefix, relName); plugin = callDep(prefix); } //Normalize according if (prefix) { if (plugin && plugin.normalize) { name = plugin.normalize(name, makeNormalize(relName)); } else { name = normalize(name, relName); } } else { name = normalize(name, relName); parts = splitPrefix(name); prefix = parts[0]; name = parts[1]; if (prefix) { plugin = callDep(prefix); } } //Using ridiculous property names for space reasons return { f: prefix ? prefix + '!' + name : name, //fullName n: name, pr: prefix, p: plugin }; }; function makeConfig(name) { return function () { return (config && config.config && config.config[name]) || {}; }; } handlers = { require: function (name) { return makeRequire(name); }, exports: function (name) { var e = defined[name]; if (typeof e !== 'undefined') { return e; } else { return (defined[name] = {}); } }, module: function (name) { return { id: name, uri: '', exports: defined[name], config: makeConfig(name) }; } }; main = function (name, deps, callback, relName) { var cjsModule, depName, ret, map, i, args = [], callbackType = typeof callback, usingExports; //Use name if no relName relName = relName || name; //Call the callback to define the module, if necessary. if (callbackType === 'undefined' || callbackType === 'function') { //Pull out the defined dependencies and pass the ordered //values to the callback. //Default to [require, exports, module] if no deps deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; for (i = 0; i < deps.length; i += 1) { map = makeMap(deps[i], relName); depName = map.f; //Fast path CommonJS standard dependencies. if (depName === "require") { args[i] = handlers.require(name); } else if (depName === "exports") { //CommonJS module spec 1.1 args[i] = handlers.exports(name); usingExports = true; } else if (depName === "module") { //CommonJS module spec 1.1 cjsModule = args[i] = handlers.module(name); } else if (hasProp(defined, depName) || hasProp(waiting, depName) || hasProp(defining, depName)) { args[i] = callDep(depName); } else if (map.p) { map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); args[i] = defined[depName]; } else { throw new Error(name + ' missing ' + depName); } } ret = callback ? callback.apply(defined[name], args) : undefined; if (name) { //If setting exports via "module" is in play, //favor that over return value and exports. After that, //favor a non-undefined return value over exports use. if (cjsModule && cjsModule.exports !== undef && cjsModule.exports !== defined[name]) { defined[name] = cjsModule.exports; } else if (ret !== undef || !usingExports) { //Use the return value from the function. defined[name] = ret; } } } else if (name) { //May just be an object definition for the module. Only //worry about defining if have a module name. defined[name] = callback; } }; requirejs = require = req = function (deps, callback, relName, forceSync, alt) { if (typeof deps === "string") { if (handlers[deps]) { //callback in this case is really relName return handlers[deps](callback); } //Just return the module wanted. In this scenario, the //deps arg is the module name, and second arg (if passed) //is just the relName. //Normalize module name, if it contains . or .. return callDep(makeMap(deps, callback).f); } else if (!deps.splice) { //deps is a config object, not an array. config = deps; if (config.deps) { req(config.deps, config.callback); } if (!callback) { return; } if (callback.splice) { //callback is an array, which means it is a dependency list. //Adjust args if there are dependencies deps = callback; callback = relName; relName = null; } else { deps = undef; } } //Support require(['a']) callback = callback || function () {}; //If relName is a function, it is an errback handler, //so remove it. if (typeof relName === 'function') { relName = forceSync; forceSync = alt; } //Simulate async callback; if (forceSync) { main(undef, deps, callback, relName); } else { //Using a non-zero value because of concern for what old browsers //do, and latest browsers "upgrade" to 4 if lower value is used: //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: //If want a value immediately, use require('id') instead -- something //that works in almond on the global level, but not guaranteed and //unlikely to work in other AMD implementations. setTimeout(function () { main(undef, deps, callback, relName); }, 4); } return req; }; /** * Just drops the config on the floor, but returns req in case * the config return value is used. */ req.config = function (cfg) { return req(cfg); }; /** * Expose module registry for debugging and tooling */ requirejs._defined = defined; define = function (name, deps, callback) { if (typeof name !== 'string') { throw new Error('See almond README: incorrect module build, no module name'); } //This module may not have dependencies if (!deps.splice) { //deps is not an array, so probably means //an object literal or factory function for //the value. Adjust args. callback = deps; deps = []; } if (!hasProp(defined, name) && !hasProp(waiting, name)) { waiting[name] = [name, deps, callback]; } }; define.amd = { jQuery: true }; }()); define("components/almond/almond.js", function(){}); /* jed.js v0.5.0beta https://github.com/SlexAxton/Jed ----------- A gettext compatible i18n library for modern JavaScript Applications by Alex Sexton - AlexSexton [at] gmail - @SlexAxton WTFPL license for use Dojo CLA for contributions Jed offers the entire applicable GNU gettext spec'd set of functions, but also offers some nicer wrappers around them. The api for gettext was written for a language with no function overloading, so Jed allows a little more of that. Many thanks to Joshua I. Miller - unrtst@cpan.org - who wrote gettext.js back in 2008. I was able to vet a lot of my ideas against his. I also made sure Jed passed against his tests in order to offer easy upgrades -- jsgettext.berlios.de */ (function (root, undef) { // Set up some underscore-style functions, if you already have // underscore, feel free to delete this section, and use it // directly, however, the amount of functions used doesn't // warrant having underscore as a full dependency. // Underscore 1.3.0 was used to port and is licensed // under the MIT License by Jeremy Ashkenas. var ArrayProto = Array.prototype, ObjProto = Object.prototype, slice = ArrayProto.slice, hasOwnProp = ObjProto.hasOwnProperty, nativeForEach = ArrayProto.forEach, breaker = {}; // We're not using the OOP style _ so we don't need the // extra level of indirection. This still means that you // sub out for real `_` though. var _ = { forEach : function( obj, iterator, context ) { var i, l, key; if ( obj === null ) { return; } if ( nativeForEach && obj.forEach === nativeForEach ) { obj.forEach( iterator, context ); } else if ( obj.length === +obj.length ) { for ( i = 0, l = obj.length; i < l; i++ ) { if ( i in obj && iterator.call( context, obj[i], i, obj ) === breaker ) { return; } } } else { for ( key in obj) { if ( hasOwnProp.call( obj, key ) ) { if ( iterator.call (context, obj[key], key, obj ) === breaker ) { return; } } } } }, extend : function( obj ) { this.forEach( slice.call( arguments, 1 ), function ( source ) { for ( var prop in source ) { obj[prop] = source[prop]; } }); return obj; } }; // END Miniature underscore impl // Jed is a constructor function var Jed = function ( options ) { // Some minimal defaults this.defaults = { "locale_data" : { "messages" : { "" : { "domain" : "messages", "lang" : "en", "plural_forms" : "nplurals=2; plural=(n != 1);" } // There are no default keys, though } }, // The default domain if one is missing "domain" : "messages" }; // Mix in the sent options with the default options this.options = _.extend( {}, this.defaults, options ); this.textdomain( this.options.domain ); if ( options.domain && ! this.options.locale_data[ this.options.domain ] ) { throw new Error('Text domain set to non-existent domain: `' + options.domain + '`'); } }; // The gettext spec sets this character as the default // delimiter for context lookups. // e.g.: context\u0004key // If your translation company uses something different, // just change this at any time and it will use that instead. Jed.context_delimiter = String.fromCharCode( 4 ); function getPluralFormFunc ( plural_form_string ) { return Jed.PF.compile( plural_form_string || "nplurals=2; plural=(n != 1);"); } function Chain( key, i18n ){ this._key = key; this._i18n = i18n; } // Create a chainable api for adding args prettily _.extend( Chain.prototype, { onDomain : function ( domain ) { this._domain = domain; return this; }, withContext : function ( context ) { this._context = context; return this; }, ifPlural : function ( num, pkey ) { this._val = num; this._pkey = pkey; return this; }, fetch : function ( sArr ) { if ( {}.toString.call( sArr ) != '[object Array]' ) { sArr = [].slice.call(arguments); } return ( sArr && sArr.length ? Jed.sprintf : function(x){ return x; } )( this._i18n.dcnpgettext(this._domain, this._context, this._key, this._pkey, this._val), sArr ); } }); // Add functions to the Jed prototype. // These will be the functions on the object that's returned // from creating a `new Jed()` // These seem redundant, but they gzip pretty well. _.extend( Jed.prototype, { // The sexier api start point translate : function ( key ) { return new Chain( key, this ); }, textdomain : function ( domain ) { if ( ! domain ) { return this._textdomain; } this._textdomain = domain; }, gettext : function ( key ) { return this.dcnpgettext.call( this, undef, undef, key ); }, dgettext : function ( domain, key ) { return this.dcnpgettext.call( this, domain, undef, key ); }, dcgettext : function ( domain , key /*, category */ ) { // Ignores the category anyways return this.dcnpgettext.call( this, domain, undef, key ); }, ngettext : function ( skey, pkey, val ) { return this.dcnpgettext.call( this, undef, undef, skey, pkey, val ); }, dngettext : function ( domain, skey, pkey, val ) { return this.dcnpgettext.call( this, domain, undef, skey, pkey, val ); }, dcngettext : function ( domain, skey, pkey, val/*, category */) { return this.dcnpgettext.call( this, domain, undef, skey, pkey, val ); }, pgettext : function ( context, key ) { return this.dcnpgettext.call( this, undef, context, key ); }, dpgettext : function ( domain, context, key ) { return this.dcnpgettext.call( this, domain, context, key ); }, dcpgettext : function ( domain, context, key/*, category */) { return this.dcnpgettext.call( this, domain, context, key ); }, npgettext : function ( context, skey, pkey, val ) { return this.dcnpgettext.call( this, undef, context, skey, pkey, val ); }, dnpgettext : function ( domain, context, skey, pkey, val ) { return this.dcnpgettext.call( this, domain, context, skey, pkey, val ); }, // The most fully qualified gettext function. It has every option. // Since it has every option, we can use it from every other method. // This is the bread and butter. // Technically there should be one more argument in this function for 'Category', // but since we never use it, we might as well not waste the bytes to define it. dcnpgettext : function ( domain, context, singular_key, plural_key, val ) { // Set some defaults plural_key = plural_key || singular_key; // Use the global domain default if one // isn't explicitly passed in domain = domain || this._textdomain; // Default the value to the singular case val = typeof val == 'undefined' ? 1 : val; var fallback; // Handle special cases // No options found if ( ! this.options ) { // There's likely something wrong, but we'll return the correct key for english // We do this by instantiating a brand new Jed instance with the default set // for everything that could be broken. fallback = new Jed(); return fallback.dcnpgettext.call( fallback, undefined, undefined, singular_key, plural_key, val ); } // No translation data provided if ( ! this.options.locale_data ) { throw new Error('No locale data provided.'); } if ( ! this.options.locale_data[ domain ] ) { throw new Error('Domain `' + domain + '` was not found.'); } if ( ! this.options.locale_data[ domain ][ "" ] ) { throw new Error('No locale meta information provided.'); } // Make sure we have a truthy key. Otherwise we might start looking // into the empty string key, which is the options for the locale // data. if ( ! singular_key ) { throw new Error('No translation key found.'); } // Handle invalid numbers, but try casting strings for good measure if ( typeof val != 'number' ) { val = parseInt( val, 10 ); if ( isNaN( val ) ) { throw new Error('The number that was passed in is not a number.'); } } var key = context ? context + Jed.context_delimiter + singular_key : singular_key, locale_data = this.options.locale_data, dict = locale_data[ domain ], pluralForms = dict[""].plural_forms || (locale_data.messages || this.defaults.locale_data.messages)[""].plural_forms, val_idx = getPluralFormFunc(pluralForms)(val) + 1, val_list, res; // Throw an error if a domain isn't found if ( ! dict ) { throw new Error('No domain named `' + domain + '` could be found.'); } val_list = dict[ key ]; // If there is no match, then revert back to // english style singular/plural with the keys passed in. if ( ! val_list || val_idx >= val_list.length ) { if (this.options.missing_key_callback) { this.options.missing_key_callback(key); } res = [ null, singular_key, plural_key ]; return res[ getPluralFormFunc(pluralForms)( val ) + 1 ]; } res = val_list[ val_idx ]; // This includes empty strings on purpose if ( ! res ) { res = [ null, singular_key, plural_key ]; return res[ getPluralFormFunc(pluralForms)( val ) + 1 ]; } return res; } }); // We add in sprintf capabilities for post translation value interolation // This is not internally used, so you can remove it if you have this // available somewhere else, or want to use a different system. // We _slightly_ modify the normal sprintf behavior to more gracefully handle // undefined values. /** sprintf() for JavaScript 0.7-beta1 http://www.diveintojavascript.com/projects/javascript-sprintf Copyright (c) Alexandru Marasteanu 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 sprintf() for JavaScript 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 Alexandru Marasteanu 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. */ var sprintf = (function() { function get_type(variable) { return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); } function str_repeat(input, multiplier) { for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */} return output.join(''); } var str_format = function() { if (!str_format.cache.hasOwnProperty(arguments[0])) { str_format.cache[arguments[0]] = str_format.parse(arguments[0]); } return str_format.format.call(null, str_format.cache[arguments[0]], arguments); }; str_format.format = function(parse_tree, argv) { var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; for (i = 0; i < tree_length; i++) { node_type = get_type(parse_tree[i]); if (node_type === 'string') { output.push(parse_tree[i]); } else if (node_type === 'array') { match = parse_tree[i]; // convenience purposes only if (match[2]) { // keyword argument arg = argv[cursor]; for (k = 0; k < match[2].length; k++) { if (!arg.hasOwnProperty(match[2][k])) { throw(sprintf('[sprintf] property "%s" does not exist', match[2][k])); } arg = arg[match[2][k]]; } } else if (match[1]) { // positional argument (explicit) arg = argv[match[1]]; } else { // positional argument (implicit) arg = argv[cursor++]; } if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) { throw(sprintf('[sprintf] expecting number but found %s', get_type(arg))); } // Jed EDIT if ( typeof arg == 'undefined' || arg === null ) { arg = ''; } // Jed EDIT switch (match[8]) { case 'b': arg = arg.toString(2); break; case 'c': arg = String.fromCharCode(arg); break; case 'd': arg = parseInt(arg, 10); break; case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; case 'o': arg = arg.toString(8); break; case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; case 'u': arg = Math.abs(arg); break; case 'x': arg = arg.toString(16); break; case 'X': arg = arg.toString(16).toUpperCase(); break; } arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; pad_length = match[6] - String(arg).length; pad = match[6] ? str_repeat(pad_character, pad_length) : ''; output.push(match[5] ? arg + pad : pad + arg); } } return output.join(''); }; str_format.cache = {}; str_format.parse = function(fmt) { var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; while (_fmt) { if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { parse_tree.push(match[0]); } else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { parse_tree.push('%'); } else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) { if (match[2]) { arg_names |= 1; var field_list = [], replacement_field = match[2], field_match = []; if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { field_list.push(field_match[1]); while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { field_list.push(field_match[1]); } else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { field_list.push(field_match[1]); } else { throw('[sprintf] huh?'); } } } else { throw('[sprintf] huh?'); } match[2] = field_list; } else { arg_names |= 2; } if (arg_names === 3) { throw('[sprintf] mixing positional and named placeholders is not (yet) supported'); } parse_tree.push(match); } else { throw('[sprintf] huh?'); } _fmt = _fmt.substring(match[0].length); } return parse_tree; }; return str_format; })(); var vsprintf = function(fmt, argv) { argv.unshift(fmt); return sprintf.apply(null, argv); }; Jed.parse_plural = function ( plural_forms, n ) { plural_forms = plural_forms.replace(/n/g, n); return Jed.parse_expression(plural_forms); }; Jed.sprintf = function ( fmt, args ) { if ( {}.toString.call( args ) == '[object Array]' ) { return vsprintf( fmt, [].slice.call(args) ); } return sprintf.apply(this, [].slice.call(arguments) ); }; Jed.prototype.sprintf = function () { return Jed.sprintf.apply(this, arguments); }; // END sprintf Implementation // Start the Plural forms section // This is a full plural form expression parser. It is used to avoid // running 'eval' or 'new Function' directly against the plural // forms. // // This can be important if you get translations done through a 3rd // party vendor. I encourage you to use this instead, however, I // also will provide a 'precompiler' that you can use at build time // to output valid/safe function representations of the plural form // expressions. This means you can build this code out for the most // part. Jed.PF = {}; Jed.PF.parse = function ( p ) { var plural_str = Jed.PF.extractPluralExpr( p ); return Jed.PF.parser.parse.call(Jed.PF.parser, plural_str); }; Jed.PF.compile = function ( p ) { // Handle trues and falses as 0 and 1 function imply( val ) { return (val === true ? 1 : val ? val : 0); } var ast = Jed.PF.parse( p ); return function ( n ) { return imply( Jed.PF.interpreter( ast )( n ) ); }; }; Jed.PF.interpreter = function ( ast ) { return function ( n ) { var res; switch ( ast.type ) { case 'GROUP': return Jed.PF.interpreter( ast.expr )( n ); case 'TERNARY': if ( Jed.PF.interpreter( ast.expr )( n ) ) { return Jed.PF.interpreter( ast.truthy )( n ); } return Jed.PF.interpreter( ast.falsey )( n ); case 'OR': return Jed.PF.interpreter( ast.left )( n ) || Jed.PF.interpreter( ast.right )( n ); case 'AND': return Jed.PF.interpreter( ast.left )( n ) && Jed.PF.interpreter( ast.right )( n ); case 'LT': return Jed.PF.interpreter( ast.left )( n ) < Jed.PF.interpreter( ast.right )( n ); case 'GT': return Jed.PF.interpreter( ast.left )( n ) > Jed.PF.interpreter( ast.right )( n ); case 'LTE': return Jed.PF.interpreter( ast.left )( n ) <= Jed.PF.interpreter( ast.right )( n ); case 'GTE': return Jed.PF.interpreter( ast.left )( n ) >= Jed.PF.interpreter( ast.right )( n ); case 'EQ': return Jed.PF.interpreter( ast.left )( n ) == Jed.PF.interpreter( ast.right )( n ); case 'NEQ': return Jed.PF.interpreter( ast.left )( n ) != Jed.PF.interpreter( ast.right )( n ); case 'MOD': return Jed.PF.interpreter( ast.left )( n ) % Jed.PF.interpreter( ast.right )( n ); case 'VAR': return n; case 'NUM': return ast.val; default: throw new Error("Invalid Token found."); } }; }; Jed.PF.extractPluralExpr = function ( p ) { // trim first p = p.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); if (! /;\s*$/.test(p)) { p = p.concat(';'); } var nplurals_re = /nplurals\=(\d+);/, plural_re = /plural\=(.*);/, nplurals_matches = p.match( nplurals_re ), res = {}, plural_matches; // Find the nplurals number if ( nplurals_matches.length > 1 ) { res.nplurals = nplurals_matches[1]; } else { throw new Error('nplurals not found in plural_forms string: ' + p ); } // remove that data to get to the formula p = p.replace( nplurals_re, "" ); plural_matches = p.match( plural_re ); if (!( plural_matches && plural_matches.length > 1 ) ) { throw new Error('`plural` expression not found: ' + p); } return plural_matches[ 1 ]; }; /* Jison generated parser */ Jed.PF.parser = (function(){ var parser = {trace: function trace() { }, yy: {}, symbols_: {"error":2,"expressions":3,"e":4,"EOF":5,"?":6,":":7,"||":8,"&&":9,"<":10,"<=":11,">":12,">=":13,"!=":14,"==":15,"%":16,"(":17,")":18,"n":19,"NUMBER":20,"$accept":0,"$end":1}, terminals_: {2:"error",5:"EOF",6:"?",7:":",8:"||",9:"&&",10:"<",11:"<=",12:">",13:">=",14:"!=",15:"==",16:"%",17:"(",18:")",19:"n",20:"NUMBER"}, productions_: [0,[3,2],[4,5],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,1],[4,1]], performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { var $0 = $$.length - 1; switch (yystate) { case 1: return { type : 'GROUP', expr: $$[$0-1] }; break; case 2:this.$ = { type: 'TERNARY', expr: $$[$0-4], truthy : $$[$0-2], falsey: $$[$0] }; break; case 3:this.$ = { type: "OR", left: $$[$0-2], right: $$[$0] }; break; case 4:this.$ = { type: "AND", left: $$[$0-2], right: $$[$0] }; break; case 5:this.$ = { type: 'LT', left: $$[$0-2], right: $$[$0] }; break; case 6:this.$ = { type: 'LTE', left: $$[$0-2], right: $$[$0] }; break; case 7:this.$ = { type: 'GT', left: $$[$0-2], right: $$[$0] }; break; case 8:this.$ = { type: 'GTE', left: $$[$0-2], right: $$[$0] }; break; case 9:this.$ = { type: 'NEQ', left: $$[$0-2], right: $$[$0] }; break; case 10:this.$ = { type: 'EQ', left: $$[$0-2], right: $$[$0] }; break; case 11:this.$ = { type: 'MOD', left: $$[$0-2], right: $$[$0] }; break; case 12:this.$ = { type: 'GROUP', expr: $$[$0-1] }; break; case 13:this.$ = { type: 'VAR' }; break; case 14:this.$ = { type: 'NUM', val: Number(yytext) }; break; } }, table: [{3:1,4:2,17:[1,3],19:[1,4],20:[1,5]},{1:[3]},{5:[1,6],6:[1,7],8:[1,8],9:[1,9],10:[1,10],11:[1,11],12:[1,12],13:[1,13],14:[1,14],15:[1,15],16:[1,16]},{4:17,17:[1,3],19:[1,4],20:[1,5]},{5:[2,13],6:[2,13],7:[2,13],8:[2,13],9:[2,13],10:[2,13],11:[2,13],12:[2,13],13:[2,13],14:[2,13],15:[2,13],16:[2,13],18:[2,13]},{5:[2,14],6:[2,14],7:[2,14],8:[2,14],9:[2,14],10:[2,14],11:[2,14],12:[2,14],13:[2,14],14:[2,14],15:[2,14],16:[2,14],18:[2,14]},{1:[2,1]},{4:18,17:[1,3],19:[1,4],20:[1,5]},{4:19,17:[1,3],19:[1,4],20:[1,5]},{4:20,17:[1,3],19:[1,4],20:[1,5]},{4:21,17:[1,3],19:[1,4],20:[1,5]},{4:22,17:[1,3],19:[1,4],20:[1,5]},{4:23,17:[1,3],19:[1,4],20:[1,5]},{4:24,17:[1,3],19:[1,4],20:[1,5]},{4:25,17:[1,3],19:[1,4],20:[1,5]},{4:26,17:[1,3],19:[1,4],20:[1,5]},{4:27,17:[1,3],19:[1,4],20:[1,5]},{6:[1,7],8:[1,8],9:[1,9],10:[1,10],11:[1,11],12:[1,12],13:[1,13],14:[1,14],15:[1,15],16:[1,16],18:[1,28]},{6:[1,7],7:[1,29],8:[1,8],9:[1,9],10:[1,10],11:[1,11],12:[1,12],13:[1,13],14:[1,14],15:[1,15],16:[1,16]},{5:[2,3],6:[2,3],7:[2,3],8:[2,3],9:[1,9],10:[1,10],11:[1,11],12:[1,12],13:[1,13],14:[1,14],15:[1,15],16:[1,16],18:[2,3]},{5:[2,4],6:[2,4],7:[2,4],8:[2,4],9:[2,4],10:[1,10],11:[1,11],12:[1,12],13:[1,13],14:[1,14],15:[1,15],16:[1,16],18:[2,4]},{5:[2,5],6:[2,5],7:[2,5],8:[2,5],9:[2,5],10:[2,5],11:[2,5],12:[2,5],13:[2,5],14:[2,5],15:[2,5],16:[1,16],18:[2,5]},{5:[2,6],6:[2,6],7:[2,6],8:[2,6],9:[2,6],10:[2,6],11:[2,6],12:[2,6],13:[2,6],14:[2,6],15:[2,6],16:[1,16],18:[2,6]},{5:[2,7],6:[2,7],7:[2,7],8:[2,7],9:[2,7],10:[2,7],11:[2,7],12:[2,7],13:[2,7],14:[2,7],15:[2,7],16:[1,16],18:[2,7]},{5:[2,8],6:[2,8],7:[2,8],8:[2,8],9:[2,8],10:[2,8],11:[2,8],12:[2,8],13:[2,8],14:[2,8],15:[2,8],16:[1,16],18:[2,8]},{5:[2,9],6:[2,9],7:[2,9],8:[2,9],9:[2,9],10:[2,9],11:[2,9],12:[2,9],13:[2,9],14:[2,9],15:[2,9],16:[1,16],18:[2,9]},{5:[2,10],6:[2,10],7:[2,10],8:[2,10],9:[2,10],10:[2,10],11:[2,10],12:[2,10],13:[2,10],14:[2,10],15:[2,10],16:[1,16],18:[2,10]},{5:[2,11],6:[2,11],7:[2,11],8:[2,11],9:[2,11],10:[2,11],11:[2,11],12:[2,11],13:[2,11],14:[2,11],15:[2,11],16:[2,11],18:[2,11]},{5:[2,12],6:[2,12],7:[2,12],8:[2,12],9:[2,12],10:[2,12],11:[2,12],12:[2,12],13:[2,12],14:[2,12],15:[2,12],16:[2,12],18:[2,12]},{4:30,17:[1,3],19:[1,4],20:[1,5]},{5:[2,2],6:[1,7],7:[2,2],8:[1,8],9:[1,9],10:[1,10],11:[1,11],12:[1,12],13:[1,13],14:[1,14],15:[1,15],16:[1,16],18:[2,2]}], defaultActions: {6:[2,1]}, parseError: function parseError(str, hash) { throw new Error(str); }, parse: function parse(input) { var self = this, stack = [0], vstack = [null], // semantic value stack lstack = [], // location stack table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; //this.reductionCount = this.shiftCount = 0; this.lexer.setInput(input); this.lexer.yy = this.yy; this.yy.lexer = this.lexer; if (typeof this.lexer.yylloc == 'undefined') this.lexer.yylloc = {}; var yyloc = this.lexer.yylloc; lstack.push(yyloc); if (typeof this.yy.parseError === 'function') this.parseError = this.yy.parseError; function popStack (n) { stack.length = stack.length - 2*n; vstack.length = vstack.length - n; lstack.length = lstack.length - n; } function lex() { var token; token = self.lexer.lex() || 1; // $end = 1 // if token isn't its numeric value, convert if (typeof token !== 'number') { token = self.symbols_[token] || token; } return token; } var symbol, preErrorSymbol, state, action, a, r, yyval={},p,len,newState, expected; while (true) { // retreive state number from top of stack state = stack[stack.length-1]; // use default actions if available if (this.defaultActions[state]) { action = this.defaultActions[state]; } else { if (symbol == null) symbol = lex(); // read action for current state and first input action = table[state] && table[state][symbol]; } // handle parse error _handle_error: if (typeof action === 'undefined' || !action.length || !action[0]) { if (!recovering) { // Report error expected = []; for (p in table[state]) if (this.terminals_[p] && p > 2) { expected.push("'"+this.terminals_[p]+"'"); } var errStr = ''; if (this.lexer.showPosition) { errStr = 'Parse error on line '+(yylineno+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+expected.join(', ') + ", got '" + this.terminals_[symbol]+ "'"; } else { errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " + (symbol == 1 /*EOF*/ ? "end of input" : ("'"+(this.terminals_[symbol] || symbol)+"'")); } this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); } // just recovered from another error if (recovering == 3) { if (symbol == EOF) { throw new Error(errStr || 'Parsing halted.'); } // discard current lookahead and grab another yyleng = this.lexer.yyleng; yytext = this.lexer.yytext; yylineno = this.lexer.yylineno; yyloc = this.lexer.yylloc; symbol = lex(); } // try to recover from error while (1) { // check for error recovery rule in this state if ((TERROR.toString()) in table[state]) { break; } if (state == 0) { throw new Error(errStr || 'Parsing halted.'); } popStack(1); state = stack[stack.length-1]; } preErrorSymbol = symbol; // save the lookahead token symbol = TERROR; // insert generic error symbol as new lookahead state = stack[stack.length-1]; action = table[state] && table[state][TERROR]; recovering = 3; // allow 3 real symbols to be shifted before reporting a new error } // this shouldn't happen, unless resolve defaults are off if (action[0] instanceof Array && action.length > 1) { throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol); } switch (action[0]) { case 1: // shift //this.shiftCount++; stack.push(symbol); vstack.push(this.lexer.yytext); lstack.push(this.lexer.yylloc); stack.push(action[1]); // push state symbol = null; if (!preErrorSymbol) { // normal execution/no error yyleng = this.lexer.yyleng; yytext = this.lexer.yytext; yylineno = this.lexer.yylineno; yyloc = this.lexer.yylloc; if (recovering > 0) recovering--; } else { // error just occurred, resume old lookahead f/ before error symbol = preErrorSymbol; preErrorSymbol = null; } break; case 2: // reduce //this.reductionCount++; len = this.productions_[action[1]][1]; // perform semantic action yyval.$ = vstack[vstack.length-len]; // default to $$ = $1 // default location, uses first token for firsts, last for lasts yyval._$ = { first_line: lstack[lstack.length-(len||1)].first_line, last_line: lstack[lstack.length-1].last_line, first_column: lstack[lstack.length-(len||1)].first_column, last_column: lstack[lstack.length-1].last_column }; r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); if (typeof r !== 'undefined') { return r; } // pop off stack if (len) { stack = stack.slice(0,-1*len*2); vstack = vstack.slice(0, -1*len); lstack = lstack.slice(0, -1*len); } stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce) vstack.push(yyval.$); lstack.push(yyval._$); // goto new state = table[STATE][NONTERMINAL] newState = table[stack[stack.length-2]][stack[stack.length-1]]; stack.push(newState); break; case 3: // accept return true; } } return true; }};/* Jison generated lexer */ var lexer = (function(){ var lexer = ({EOF:1, parseError:function parseError(str, hash) { if (this.yy.parseError) { this.yy.parseError(str, hash); } else { throw new Error(str); } }, setInput:function (input) { this._input = input; this._more = this._less = this.done = false; this.yylineno = this.yyleng = 0; this.yytext = this.matched = this.match = ''; this.conditionStack = ['INITIAL']; this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; return this; }, input:function () { var ch = this._input[0]; this.yytext+=ch; this.yyleng++; this.match+=ch; this.matched+=ch; var lines = ch.match(/\n/); if (lines) this.yylineno++; this._input = this._input.slice(1); return ch; }, unput:function (ch) { this._input = ch + this._input; return this; }, more:function () { this._more = true; return this; }, pastInput:function () { var past = this.matched.substr(0, this.matched.length - this.match.length); return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); }, upcomingInput:function () { var next = this.match; if (next.length < 20) { next += this._input.substr(0, 20-next.length); } return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); }, showPosition:function () { var pre = this.pastInput(); var c = new Array(pre.length + 1).join("-"); return pre + this.upcomingInput() + "\n" + c+"^"; }, next:function () { if (this.done) { return this.EOF; } if (!this._input) this.done = true; var token, match, col, lines; if (!this._more) { this.yytext = ''; this.match = ''; } var rules = this._currentRules(); for (var i=0;i < rules.length; i++) { match = this._input.match(this.rules[rules[i]]); if (match) { lines = match[0].match(/\n.*/g); if (lines) this.yylineno += lines.length; this.yylloc = {first_line: this.yylloc.last_line, last_line: this.yylineno+1, first_column: this.yylloc.last_column, last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length} this.yytext += match[0]; this.match += match[0]; this.matches = match; this.yyleng = this.yytext.length; this._more = false; this._input = this._input.slice(match[0].length); this.matched += match[0]; token = this.performAction.call(this, this.yy, this, rules[i],this.conditionStack[this.conditionStack.length-1]); if (token) return token; else return; } } if (this._input === "") { return this.EOF; } else { this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), {text: "", token: null, line: this.yylineno}); } }, lex:function lex() { var r = this.next(); if (typeof r !== 'undefined') { return r; } else { return this.lex(); } }, begin:function begin(condition) { this.conditionStack.push(condition); }, popState:function popState() { return this.conditionStack.pop(); }, _currentRules:function _currentRules() { return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; }, topState:function () { return this.conditionStack[this.conditionStack.length-2]; }, pushState:function begin(condition) { this.begin(condition); }}); lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { var YYSTATE=YY_START; switch($avoiding_name_collisions) { case 0:/* skip whitespace */ break; case 1:return 20 break; case 2:return 19 break; case 3:return 8 break; case 4:return 9 break; case 5:return 6 break; case 6:return 7 break; case 7:return 11 break; case 8:return 13 break; case 9:return 10 break; case 10:return 12 break; case 11:return 14 break; case 12:return 15 break; case 13:return 16 break; case 14:return 17 break; case 15:return 18 break; case 16:return 5 break; case 17:return 'INVALID' break; } }; lexer.rules = [/^\s+/,/^[0-9]+(\.[0-9]+)?\b/,/^n\b/,/^\|\|/,/^&&/,/^\?/,/^:/,/^<=/,/^>=/,/^/,/^!=/,/^==/,/^%/,/^\(/,/^\)/,/^$/,/^./]; lexer.conditions = {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],"inclusive":true}};return lexer;})() parser.lexer = lexer; return parser; })(); // End parser // Handle node, amd, and global systems if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = Jed; } exports.Jed = Jed; } else { if (typeof define === 'function' && define.amd) { define('jed', [],function() { return Jed; }); } // Leak a global regardless of module system root['Jed'] = Jed; } })(this); /** * @license RequireJS text 2.0.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/requirejs/text for details */ /*jslint regexp: true */ /*global require, XMLHttpRequest, ActiveXObject, define, window, process, Packages, java, location, Components, FileUtils */ define('text',['module'], function (module) { 'use strict'; var text, fs, Cc, Ci, xpcIsWindows, progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, hasLocation = typeof location !== 'undefined' && location.href, defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), defaultHostName = hasLocation && location.hostname, defaultPort = hasLocation && (location.port || undefined), buildMap = {}, masterConfig = (module.config && module.config()) || {}; text = { version: '2.0.14', strip: function (content) { //Strips declarations so that external SVG and XML //documents can be added to a document without worry. Also, if the string //is an HTML document, only the part inside the body tag is returned. if (content) { content = content.replace(xmlRegExp, ""); var matches = content.match(bodyRegExp); if (matches) { content = matches[1]; } } else { content = ""; } return content; }, jsEscape: function (content) { return content.replace(/(['\\])/g, '\\$1') .replace(/[\f]/g, "\\f") .replace(/[\b]/g, "\\b") .replace(/[\n]/g, "\\n") .replace(/[\t]/g, "\\t") .replace(/[\r]/g, "\\r") .replace(/[\u2028]/g, "\\u2028") .replace(/[\u2029]/g, "\\u2029"); }, createXhr: masterConfig.createXhr || function () { //Would love to dump the ActiveX crap in here. Need IE 6 to die first. var xhr, i, progId; if (typeof XMLHttpRequest !== "undefined") { return new XMLHttpRequest(); } else if (typeof ActiveXObject !== "undefined") { for (i = 0; i < 3; i += 1) { progId = progIds[i]; try { xhr = new ActiveXObject(progId); } catch (e) {} if (xhr) { progIds = [progId]; // so faster next time break; } } } return xhr; }, /** * Parses a resource name into its component parts. Resource names * look like: module/name.ext!strip, where the !strip part is * optional. * @param {String} name the resource name * @returns {Object} with properties "moduleName", "ext" and "strip" * where strip is a boolean. */ parseName: function (name) { var modName, ext, temp, strip = false, index = name.lastIndexOf("."), isRelative = name.indexOf('./') === 0 || name.indexOf('../') === 0; if (index !== -1 && (!isRelative || index > 1)) { modName = name.substring(0, index); ext = name.substring(index + 1); } else { modName = name; } temp = ext || modName; index = temp.indexOf("!"); if (index !== -1) { //Pull off the strip arg. strip = temp.substring(index + 1) === "strip"; temp = temp.substring(0, index); if (ext) { ext = temp; } else { modName = temp; } } return { moduleName: modName, ext: ext, strip: strip }; }, xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, /** * Is an URL on another domain. Only works for browser use, returns * false in non-browser environments. Only used to know if an * optimized .js version of a text resource should be loaded * instead. * @param {String} url * @returns Boolean */ useXhr: function (url, protocol, hostname, port) { var uProtocol, uHostName, uPort, match = text.xdRegExp.exec(url); if (!match) { return true; } uProtocol = match[2]; uHostName = match[3]; uHostName = uHostName.split(':'); uPort = uHostName[1]; uHostName = uHostName[0]; return (!uProtocol || uProtocol === protocol) && (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && ((!uPort && !uHostName) || uPort === port); }, finishLoad: function (name, strip, content, onLoad) { content = strip ? text.strip(content) : content; if (masterConfig.isBuild) { buildMap[name] = content; } onLoad(content); }, load: function (name, req, onLoad, config) { //Name has format: some.module.filext!strip //The strip part is optional. //if strip is present, then that means only get the string contents //inside a body tag in an HTML string. For XML/SVG content it means //removing the declarations so the content can be inserted //into the current doc without problems. // Do not bother with the work if a build and text will // not be inlined. if (config && config.isBuild && !config.inlineText) { onLoad(); return; } masterConfig.isBuild = config && config.isBuild; var parsed = text.parseName(name), nonStripName = parsed.moduleName + (parsed.ext ? '.' + parsed.ext : ''), url = req.toUrl(nonStripName), useXhr = (masterConfig.useXhr) || text.useXhr; // Do not load if it is an empty: url if (url.indexOf('empty:') === 0) { onLoad(); return; } //Load the text. Use XHR if possible and in a browser. if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { text.get(url, function (content) { text.finishLoad(name, parsed.strip, content, onLoad); }, function (err) { if (onLoad.error) { onLoad.error(err); } }); } else { //Need to fetch the resource across domains. Assume //the resource has been optimized into a JS module. Fetch //by the module name + extension, but do not include the //!strip part to avoid file system issues. req([nonStripName], function (content) { text.finishLoad(parsed.moduleName + '.' + parsed.ext, parsed.strip, content, onLoad); }); } }, write: function (pluginName, moduleName, write, config) { if (buildMap.hasOwnProperty(moduleName)) { var content = text.jsEscape(buildMap[moduleName]); write.asModule(pluginName + "!" + moduleName, "define(function () { return '" + content + "';});\n"); } }, writeFile: function (pluginName, moduleName, req, write, config) { var parsed = text.parseName(moduleName), extPart = parsed.ext ? '.' + parsed.ext : '', nonStripName = parsed.moduleName + extPart, //Use a '.js' file name so that it indicates it is a //script that can be loaded across domains. fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; //Leverage own load() method to load plugin value, but only //write out values that do not have the strip argument, //to avoid any potential issues with ! in file names. text.load(nonStripName, req, function (value) { //Use own write() method to construct full module value. //But need to create shell that translates writeFile's //write() to the right interface. var textWrite = function (contents) { return write(fileName, contents); }; textWrite.asModule = function (moduleName, contents) { return write.asModule(moduleName, fileName, contents); }; text.write(pluginName, nonStripName, textWrite, config); }, config); } }; if (masterConfig.env === 'node' || (!masterConfig.env && typeof process !== "undefined" && process.versions && !!process.versions.node && !process.versions['node-webkit'] && !process.versions['atom-shell'])) { //Using special require.nodeRequire, something added by r.js. fs = require.nodeRequire('fs'); text.get = function (url, callback, errback) { try { var file = fs.readFileSync(url, 'utf8'); //Remove BOM (Byte Mark Order) from utf8 files if it is there. if (file[0] === '\uFEFF') { file = file.substring(1); } callback(file); } catch (e) { if (errback) { errback(e); } } }; } else if (masterConfig.env === 'xhr' || (!masterConfig.env && text.createXhr())) { text.get = function (url, callback, errback, headers) { var xhr = text.createXhr(), header; xhr.open('GET', url, true); //Allow plugins direct access to xhr headers if (headers) { for (header in headers) { if (headers.hasOwnProperty(header)) { xhr.setRequestHeader(header.toLowerCase(), headers[header]); } } } //Allow overrides specified in config if (masterConfig.onXhr) { masterConfig.onXhr(xhr, url); } xhr.onreadystatechange = function (evt) { var status, err; //Do not explicitly handle errors, those should be //visible via console output in the browser. if (xhr.readyState === 4) { status = xhr.status || 0; if (status > 399 && status < 600) { //An http 4xx or 5xx error. Signal an error. err = new Error(url + ' HTTP status: ' + status); err.xhr = xhr; if (errback) { errback(err); } } else { callback(xhr.responseText); } if (masterConfig.onXhrComplete) { masterConfig.onXhrComplete(xhr, url); } } }; xhr.send(null); }; } else if (masterConfig.env === 'rhino' || (!masterConfig.env && typeof Packages !== 'undefined' && typeof java !== 'undefined')) { //Why Java, why is this so awkward? text.get = function (url, callback) { var stringBuffer, line, encoding = "utf-8", file = new java.io.File(url), lineSeparator = java.lang.System.getProperty("line.separator"), input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), content = ''; try { stringBuffer = new java.lang.StringBuffer(); line = input.readLine(); // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 // http://www.unicode.org/faq/utf_bom.html // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 if (line && line.length() && line.charAt(0) === 0xfeff) { // Eat the BOM, since we've already found the encoding on this file, // and we plan to concatenating this buffer with others; the BOM should // only appear at the top of a file. line = line.substring(1); } if (line !== null) { stringBuffer.append(line); } while ((line = input.readLine()) !== null) { stringBuffer.append(lineSeparator); stringBuffer.append(line); } //Make sure we return a JavaScript string and not a Java string. content = String(stringBuffer.toString()); //String } finally { input.close(); } callback(content); }; } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && typeof Components !== 'undefined' && Components.classes && Components.interfaces)) { //Avert your gaze! Cc = Components.classes; Ci = Components.interfaces; Components.utils['import']('resource://gre/modules/FileUtils.jsm'); xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); text.get = function (url, callback) { var inStream, convertStream, fileObj, readData = {}; if (xpcIsWindows) { url = url.replace(/\//g, '\\'); } fileObj = new FileUtils.File(url); //XPCOM, you so crazy try { inStream = Cc['@mozilla.org/network/file-input-stream;1'] .createInstance(Ci.nsIFileInputStream); inStream.init(fileObj, 1, 0, false); convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] .createInstance(Ci.nsIConverterInputStream); convertStream.init(inStream, "utf-8", inStream.available(), Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); convertStream.readString(inStream.available(), readData); convertStream.close(); inStream.close(); callback(readData.value); } catch (e) { throw new Error((fileObj && fileObj.path || '') + ': ' + e); } }; } return text; }); // RequireJS UnderscoreJS template plugin // http://github.com/jfparadis/requirejs-tpl // // An alternative to http://github.com/ZeeAgency/requirejs-tpl // // Using UnderscoreJS micro-templates at http://underscorejs.org/#template // Using and RequireJS text.js at http://requirejs.org/docs/api.html#text // @author JF Paradis // @version 0.0.2 // // Released under the MIT license // // Usage: // require(['backbone', 'tpl!mytemplate'], function (Backbone, mytemplate) { // return Backbone.View.extend({ // initialize: function(){ // this.render(); // }, // render: function(){ // this.$el.html(mytemplate({message: 'hello'})); // }); // }); // // Configuration: (optional) // require.config({ // tpl: { // extension: '.tpl' // default = '.html' // } // }); /*jslint nomen: true */ /*global define: false */ define('tpl',['text', 'underscore'], function (text, _) { 'use strict'; var buildMap = {}, buildTemplateSource = "define('{pluginName}!{moduleName}', function () { return {source}; });\n"; return { version: '0.0.2', load: function (moduleName, parentRequire, onload, config) { if (config.tpl && config.tpl.templateSettings) { _.templateSettings = config.tpl.templateSettings; } if (buildMap[moduleName]) { onload(buildMap[moduleName]); } else { var ext = (config.tpl && config.tpl.extension) || '.html'; var path = (config.tpl && config.tpl.path) || ''; text.load(path + moduleName + ext, parentRequire, function (source) { buildMap[moduleName] = _.template(source); onload(buildMap[moduleName]); }, config); } }, write: function (pluginName, moduleName, write) { var build = buildMap[moduleName], source = build && build.source; if (source) { write.asModule(pluginName + '!' + moduleName, buildTemplateSource .replace('{pluginName}', pluginName) .replace('{moduleName}', moduleName) .replace('{source}', source)); } } }; }); define('tpl!action', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
\n '+ ((__t=(time))==null?'':__t)+ ' **'+ ((__t=(username))==null?'':__t)+ ' \n '+ ((__t=(message))==null?'':__t)+ '\n
\n'; } return __p; }; }); define('tpl!add_contact_dropdown', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='\n'; } return __p; }; }); define('tpl!add_contact_form', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
  • \n
    \n \n \n
    \n
  • \n'; } return __p; }; }); define('tpl!change_status_message', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
    \n
    \n \n \n \n \n
    \n
    \n'; } return __p; }; }); define('tpl!chat_status', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='\n'; } return __p; }; }); define('tpl!chatarea', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
    \n
    \n
    \n '; if (show_toolbar) { __p+='\n
      \n '; } __p+='\n \n'; } return __p; }; }); define('tpl!form_username', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+=''; if (label) { __p+='\n\n'; } __p+='\n
      \n '+ ((__t=(domain))==null?'':__t)+ '\n
      \n'; } return __p; }; }); define('tpl!group_header', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+=''+ ((__t=(label_group))==null?'':__t)+ '\n'; } return __p; }; }); define('tpl!info', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
      '+ ((__t=(message))==null?'':__t)+ '
      \n'; } return __p; }; }); define('tpl!login_panel', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='\n '; if (auto_login) { __p+='\n
      ')); } if (spinner === true) { this.$content.append(''); } else if (spinner === false) { this.$content.find('span.spinner').remove(); } return this.scrollDown(); }, onMessageAdded: function (message) { /* Handler that gets called when a new message object is created. * * Parameters: * (Object) message - The message Backbone object that was added. */ if (typeof this.clear_status_timeout !== 'undefined') { window.clearTimeout(this.clear_status_timeout); delete this.clear_status_timeout; } if (!message.get('message')) { if (message.get('chat_state') === COMPOSING) { this.showStatusNotification(message.get('fullname')+' '+__('is typing')); this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 10000); return; } else if (message.get('chat_state') === PAUSED) { this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing')); return; } else if (_.contains([INACTIVE, ACTIVE], message.get('chat_state'))) { this.$content.find('div.chat-event').remove(); return; } else if (message.get('chat_state') === GONE) { this.showStatusNotification(message.get('fullname')+' '+__('has gone away')); return; } } else { this.showMessage(_.clone(message.attributes)); } if ((message.get('sender') !== 'me') && (converse.windowState === 'blur')) { converse.incrementMsgCounter(); } if (!this.model.get('minimized') && !this.$el.is(':visible')) { this.show(); } }, createMessageStanza: function (message) { return $msg({ from: converse.connection.jid, to: this.model.get('jid'), type: 'chat', id: message.get('msgid') }).c('body').t(message.get('message')).up() .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); }, sendMessage: function (message) { /* Responsible for sending off a text message. * * Parameters: * (Message) message - The chat message */ // TODO: We might want to send to specfic resources. // Especially in the OTR case. var messageStanza = this.createMessageStanza(message); converse.connection.send(messageStanza); if (converse.forward_messages) { // Forward the message, so that other connected resources are also aware of it. converse.connection.send( $msg({ to: converse.bare_jid, type: 'chat', id: message.get('msgid') }) .c('forwarded', {xmlns:'urn:xmpp:forward:0'}) .c('delay', {xmns:'urn:xmpp:delay',stamp:(new Date()).getTime()}).up() .cnode(messageStanza.tree()) ); } }, onMessageSubmitted: function (text) { /* This method gets called once the user has typed a message * and then pressed enter in a chat box. * * Parameters: * (string) text - The chat message text. */ if (!converse.connection.authenticated) { return this.showHelpMessages( ['Sorry, the connection has been lost, '+ 'and your message could not be sent'], 'error' ); } var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs; if (match) { if (match[1] === "clear") { return this.clearMessages(); } else if (match[1] === "help") { msgs = [ '/help:'+__('Show this menu')+'', '/me:'+__('Write in the third person')+'', '/clear:'+__('Remove messages')+'' ]; this.showHelpMessages(msgs); return; } } var fullname = converse.xmppstatus.get('fullname'); fullname = _.isEmpty(fullname)? converse.bare_jid: fullname; var message = this.model.messages.create({ fullname: fullname, sender: 'me', time: moment().format(), message: text }); this.sendMessage(message); }, sendChatState: function () { /* Sends a message with the status of the user in this chat session * as taken from the 'chat_state' attribute of the chat box. * See XEP-0085 Chat State Notifications. */ converse.connection.send( $msg({'to':this.model.get('jid'), 'type': 'chat'}) .c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}) ); }, setChatState: function (state, no_save) { /* Mutator for setting the chat state of this chat session. * Handles clearing of any chat state notification timeouts and * setting new ones if necessary. * Timeouts are set when the state being set is COMPOSING or PAUSED. * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE. * See XEP-0085 Chat State Notifications. * * Parameters: * (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE) * (Boolean) no_save - Just do the cleanup or setup but don't actually save the state. */ if (typeof this.chat_state_timeout !== 'undefined') { window.clearTimeout(this.chat_state_timeout); delete this.chat_state_timeout; } if (state === COMPOSING) { this.chat_state_timeout = window.setTimeout( this.setChatState.bind(this), converse.TIMEOUTS.PAUSED, PAUSED); } else if (state === PAUSED) { this.chat_state_timeout = window.setTimeout( this.setChatState.bind(this), converse.TIMEOUTS.INACTIVE, INACTIVE); } if (!no_save && this.model.get('chat_state') !== state) { this.model.set('chat_state', state); } return this; }, keyPressed: function (ev) { /* Event handler for when a key is pressed in a chat box textarea. */ var $textarea = $(ev.target), message; if (ev.keyCode === KEY.ENTER) { ev.preventDefault(); message = $textarea.val(); $textarea.val('').focus(); if (message !== '') { if (this.model.get('chatroom')) { this.onChatRoomMessageSubmitted(message); } else { this.onMessageSubmitted(message); } converse.emit('messageSend', message); } this.setChatState(ACTIVE); } else if (!this.model.get('chatroom')) { // chat state data is currently only for single user chat // Set chat state to composing if keyCode is not a forward-slash // (which would imply an internal command and not a message). this.setChatState(COMPOSING, ev.keyCode === KEY.FORWARD_SLASH); } }, onStartVerticalResize: function (ev) { if (!converse.allow_dragresize) { return true; } // Record element attributes for mouseMove(). this.height = this.$el.children('.box-flyout').height(); converse.resizing = { 'chatbox': this, 'direction': 'top' }; this.prev_pageY = ev.pageY; }, onStartHorizontalResize: function (ev) { if (!converse.allow_dragresize) { return true; } this.width = this.$el.children('.box-flyout').width(); converse.resizing = { 'chatbox': this, 'direction': 'left' }; this.prev_pageX = ev.pageX; }, onStartDiagonalResize: function (ev) { this.onStartHorizontalResize(ev); this.onStartVerticalResize(ev); converse.resizing.direction = 'topleft'; }, setChatBoxHeight: function (height) { if (!this.model.get('minimized')) { if (height) { height = converse.applyDragResistance(height, this.model.get('default_height'))+'px'; } else { height = ""; } this.$el.children('.box-flyout')[0].style.height = height; } }, setChatBoxWidth: function (width) { if (!this.model.get('minimized')) { if (width) { width = converse.applyDragResistance(width, this.model.get('default_width'))+'px'; } else { width = ""; } this.$el[0].style.width = width; this.$el.children('.box-flyout')[0].style.width = width; } }, resizeChatBox: function (ev) { var diff; if (converse.resizing.direction.indexOf('top') === 0) { diff = ev.pageY - this.prev_pageY; if (diff) { this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height'); this.prev_pageY = ev.pageY; this.setChatBoxHeight(this.height); } } if (converse.resizing.direction.indexOf('left') !== -1) { diff = this.prev_pageX - ev.pageX; if (diff) { this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width'); this.prev_pageX = ev.pageX; this.setChatBoxWidth(this.width); } } }, clearMessages: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); if (result === true) { this.$content.empty(); this.model.messages.reset(); this.model.messages.browserStorage._clear(); } return this; }, insertEmoticon: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); var $textbox = this.$el.find('textarea.chat-textarea'); var value = $textbox.val(); var $target = $(ev.target); $target = $target.is('a') ? $target : $target.children('a'); if (value && (value[value.length-1] !== ' ')) { value = value + ' '; } $textbox.focus().val(value+$target.data('emoticon')+' '); }, toggleEmoticonMenu: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); }, toggleCall: function (ev) { ev.stopPropagation(); converse.emit('callButtonClicked', { connection: converse.connection, model: this.model }); }, onChatStatusChanged: function (item) { var chat_status = item.get('chat_status'), fullname = item.get('fullname'); fullname = _.isEmpty(fullname)? item.get('jid'): fullname; if (this.$el.is(':visible')) { if (chat_status === 'offline') { this.showStatusNotification(fullname+' '+__('has gone offline')); } else if (chat_status === 'away') { this.showStatusNotification(fullname+' '+__('has gone away')); } else if ((chat_status === 'dnd')) { this.showStatusNotification(fullname+' '+__('is busy')); } else if (chat_status === 'online') { this.$el.find('div.chat-event').remove(); } } converse.emit('contactStatusChanged', item.attributes, item.get('chat_status')); }, onStatusChanged: function (item) { this.showStatusMessage(); converse.emit('contactStatusMessageChanged', item.attributes, item.get('status')); }, onMinimizedChanged: function (item) { if (item.get('minimized')) { this.hide(); } else { this.maximize(); } }, showStatusMessage: function (msg) { msg = msg || this.model.get('status'); if (typeof msg === "string") { this.$el.find('p.user-custom-message').text(msg).attr('title', msg); } return this; }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (converse.connection.connected) { this.model.destroy(); this.setChatState(INACTIVE); } else { this.hide(); } converse.emit('chatBoxClosed', this); return this; }, maximize: function () { var chatboxviews = converse.chatboxviews; // Restores a minimized chat box this.$el.insertAfter(chatboxviews.get("controlbox").$el).show('fast', function () { /* Now that the chat box is visible, we can call trimChats * to make space available if need be. */ chatboxviews.trimChats(this); utils.refreshWebkit(); this.$content.scrollTop(this.model.get('scroll')); this.setChatState(ACTIVE).focus(); converse.emit('chatBoxMaximized', this); }.bind(this)); }, minimize: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } // save the scroll position to restore it on maximize this.model.save({'scroll': this.$content.scrollTop()}); // Minimizes a chat box this.setChatState(INACTIVE).model.minimize(); this.$el.hide('fast', utils.refreshwebkit); converse.emit('chatBoxMinimized', this); }, updateVCard: function () { if (!this.use_vcards) { return this; } var jid = this.model.get('jid'), contact = converse.roster.get(jid); if ((contact) && (!contact.get('vcard_updated'))) { converse.getVCard( jid, function (iq, jid, fullname, image, image_type, url) { this.model.save({ 'fullname' : fullname || jid, 'url': url, 'image_type': image_type, 'image': image }); }.bind(this), function () { converse.log("ChatBoxView.initialize: An error occured while fetching vcard"); } ); } return this; }, renderToolbar: function (options) { if (!converse.show_toolbar) { return; } options = _.extend(options || {}, { label_clear: __('Clear all messages'), label_hide_occupants: __('Hide the list of occupants'), label_insert_smiley: __('Insert a smiley'), label_start_call: __('Start a call'), show_call_button: converse.visible_toolbar_buttons.call, show_clear_button: converse.visible_toolbar_buttons.clear, show_emoticons: converse.visible_toolbar_buttons.emoticons, // FIXME Leaky abstraction MUC show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants }); this.$el.find('.chat-toolbar').html(converse.templates.toolbar(_.extend(this.model.toJSON(), options || {}))); return this; }, renderAvatar: function () { if (!this.model.get('image')) { return; } var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'), canvas = $('').get(0); if (!(canvas.getContext && canvas.getContext('2d'))) { return this; } var ctx = canvas.getContext('2d'); var img = new Image(); // Create new Image object img.onload = function () { var ratio = img.width/img.height; if (ratio < 1) { ctx.drawImage(img, 0,0, 32, 32*(1/ratio)); } else { ctx.drawImage(img, 0,0, 32, 32*ratio); } }; img.src = img_src; this.$el.find('.chat-title').before(canvas); return this; }, focus: function () { this.$el.find('.chat-textarea').focus(); converse.emit('chatBoxFocused', this); return this; }, hide: function () { if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { this.$el.hide(); utils.refreshWebkit(); } return this; }, show: _.debounce(function (focus) { if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { if (focus) { this.focus(); } return this; } this.initDragResize().setDimensions(); this.$el.fadeIn(function () { if (converse.connection.connected) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); } this.setChatState(ACTIVE); this.scrollDown(); if (focus) { this.focus(); } }.bind(this)); return this; }, 250, true), scrollDownMessageHeight: function ($message) { if (this.$content.is(':visible')) { this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight); } return this; }, scrollDown: function () { if (this.$content.is(':visible')) { this.$content.scrollTop(this.$content[0].scrollHeight); } return this; } }); this.ContactsPanel = Backbone.View.extend({ tagName: 'div', className: 'controlbox-pane', id: 'users', events: { 'click a.toggle-xmpp-contact-form': 'toggleContactForm', 'submit form.add-xmpp-contact': 'addContactFromForm', 'submit form.search-xmpp-contact': 'searchContacts', 'click a.subscribe-to-user': 'addContactFromList' }, initialize: function (cfg) { cfg.$parent.append(this.$el); this.$tabs = cfg.$parent.parent().find('#controlbox-tabs'); }, render: function () { var markup; var widgets = converse.templates.contacts_panel({ label_online: __('Online'), label_busy: __('Busy'), label_away: __('Away'), label_offline: __('Offline'), label_logout: __('Log out'), include_offline_state: converse.include_offline_state, allow_logout: converse.allow_logout }); this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS})); if (converse.xhr_user_search) { markup = converse.templates.search_contact({ label_contact_name: __('Contact name'), label_search: __('Search') }); } else { markup = converse.templates.add_contact_form({ label_contact_username: __('e.g. user@example.com'), label_add: __('Add') }); } if (converse.allow_contact_requests) { widgets += converse.templates.add_contact_dropdown({ label_click_to_chat: __('Click to add new chat contacts'), label_add_contact: __('Add a contact') }); } this.$el.html(widgets); this.$el.find('.search-xmpp ul').append(markup); return this; }, toggleContactForm: function (ev) { ev.preventDefault(); this.$el.find('.search-xmpp').toggle('fast', function () { if ($(this).is(':visible')) { $(this).find('input.username').focus(); } }); }, searchContacts: function (ev) { ev.preventDefault(); $.getJSON(converse.xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) { var $ul= $('.search-xmpp ul'); $ul.find('li.found-user').remove(); $ul.find('li.chat-info').remove(); if (!data.length) { $ul.append('
    • '+__('No users found')+'
    • '); } $(data).each(function (idx, obj) { $ul.append( $('
    • ') .append( $('') .attr('data-recipient', Strophe.getNodeFromJid(obj.id)+"@"+Strophe.getDomainFromJid(obj.id)) .text(obj.fullname) ) ); }); }); }, addContactFromForm: function (ev) { ev.preventDefault(); var $input = $(ev.target).find('input'); var jid = $input.val(); if (! jid) { // this is not a valid JID $input.addClass('error'); return; } converse.roster.addAndSubscribe(jid); $('.search-xmpp').hide(); }, addContactFromList: function (ev) { ev.preventDefault(); var $target = $(ev.target), jid = $target.attr('data-recipient'), name = $target.text(); converse.roster.addAndSubscribe(jid, name); $target.parent().remove(); $('.search-xmpp').hide(); } }); this.ControlBoxView = converse.ChatBoxView.extend({ tagName: 'div', className: 'chatbox', id: 'controlbox', events: { 'click a.close-chatbox-button': 'close', 'click ul#controlbox-tabs li a': 'switchTab', 'mousedown .dragresize-top': 'onStartVerticalResize', 'mousedown .dragresize-left': 'onStartHorizontalResize', 'mousedown .dragresize-topleft': 'onStartDiagonalResize' }, initialize: function () { this.$el.insertAfter(converse.controlboxtoggle.$el); $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100)); this.model.on('change:connected', this.onConnected, this); this.model.on('destroy', this.hide, this); this.model.on('hide', this.hide, this); this.model.on('show', this.show, this); this.model.on('change:closed', this.ensureClosedState, this); this.render(); if (this.model.get('connected')) { this.initRoster(); } if (typeof this.model.get('closed')==='undefined') { this.model.set('closed', !converse.show_controlbox_by_default); } if (!this.model.get('closed')) { this.show(); } else { this.hide(); } }, render: function () { if (!converse.connection.connected || !converse.connection.authenticated || converse.connection.disconnecting) { // TODO: we might need to take prebinding into consideration here. this.renderLoginPanel(); } else if (!this.contactspanel || !this.contactspanel.$el.is(':visible')) { this.renderContactsPanel(); } return this; }, giveFeedback: function (message, klass) { var $el = this.$('.conn-feedback'); $el.addClass('conn-feedback').text(message); if (klass) { $el.addClass(klass); } }, onConnected: function () { if (this.model.get('connected')) { this.render().initRoster(); } }, initRoster: function () { /* We initialize the roster, which will appear inside the * Contacts Panel. */ converse.roster = new converse.RosterContacts(); converse.roster.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.contacts-'+converse.bare_jid)); var rostergroups = new converse.RosterGroups(); rostergroups.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.roster.groups'+converse.bare_jid)); converse.rosterview = new converse.RosterView({model: rostergroups}); this.contactspanel.$el.append(converse.rosterview.$el); converse.rosterview.render().fetch().update(); return this; }, renderLoginPanel: function () { var $feedback = this.$('.conn-feedback'); // we want to still show any existing feedback. this.$el.html(converse.templates.controlbox(this.model.toJSON())); var cfg = { '$parent': this.$el.find('.controlbox-panes'), 'model': this }; if (!this.loginpanel) { this.loginpanel = new converse.LoginPanel(cfg); } else { this.loginpanel.delegateEvents().initialize(cfg); } this.loginpanel.render(); this.initDragResize().setDimensions(); if ($feedback.length && $feedback.text() !== __('Connecting')) { this.$('.conn-feedback').replaceWith($feedback); } return this; }, renderContactsPanel: function () { this.$el.html(converse.templates.controlbox(this.model.toJSON())); this.contactspanel = new converse.ContactsPanel({ '$parent': this.$el.find('.controlbox-panes') }); this.contactspanel.render(); converse.xmppstatusview = new converse.XMPPStatusView({ 'model': converse.xmppstatus }); converse.xmppstatusview.render(); this.initDragResize().setDimensions(); }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (converse.connection.connected) { this.model.save({'closed': true}); } else { this.model.trigger('hide'); } converse.emit('controlBoxClosed', this); return this; }, ensureClosedState: function () { if (this.model.get('closed')) { this.hide(); } else { this.show(); } }, hide: function (callback) { this.$el.hide('fast', function () { utils.refreshWebkit(); converse.emit('chatBoxClosed', this); converse.controlboxtoggle.show(function () { if (typeof callback === "function") { callback(); } }); }); return this; }, show: function () { converse.controlboxtoggle.hide(function () { this.$el.show('fast', function () { if (converse.rosterview) { converse.rosterview.update(); } utils.refreshWebkit(); }.bind(this)); converse.emit('controlBoxOpened', this); }.bind(this)); return this; }, switchTab: function (ev) { // TODO: automatically focus the relevant input if (ev && ev.preventDefault) { ev.preventDefault(); } var $tab = $(ev.target), $sibling = $tab.parent().siblings('li').children('a'), $tab_panel = $($tab.attr('href')); $($sibling.attr('href')).hide(); $sibling.removeClass('current'); $tab.addClass('current'); $tab_panel.show(); return this; }, showHelpMessages: function (msgs) { // Override showHelpMessages in ChatBoxView, for now do nothing. return; } }); this.ChatBoxes = Backbone.Collection.extend({ model: converse.ChatBox, comparator: 'time_opened', registerMessageHandler: function () { converse.connection.addHandler( function (message) { this.onMessage(message); return true; }.bind(this), null, 'message', 'chat'); }, onConnected: function () { this.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.chatboxes-'+converse.bare_jid)); this.registerMessageHandler(); this.fetch({ add: true, success: function (collection, resp) { collection.each(function (chatbox) { if (chatbox.get('id') !== 'controlbox' && !chatbox.get('minimized')) { chatbox.trigger('show'); } }); if (!_.include(_.pluck(resp, 'id'), 'controlbox')) { this.add({ id: 'controlbox', box_id: 'controlbox' }); } this.get('controlbox').save({connected:true}); }.bind(this) }); }, onMessage: function (message) { /* Handler method for all incoming single-user chat "message" stanzas. */ var $message = $(message), contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, msgid, chatbox, resource, from_jid = $message.attr('from'), to_jid = $message.attr('to'), to_resource = Strophe.getResourceFromJid(to_jid), archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id'); if (to_resource && to_resource !== converse.resource) { converse.log('Ignore incoming message intended for a different resource: '+to_jid, 'info'); return true; } if (from_jid === converse.connection.jid) { // FIXME: Forwarded messages should be sent to specific resources, not broadcasted converse.log("Ignore incoming message sent from this client's JID: "+from_jid, 'info'); return true; } $forwarded = $message.find('forwarded'); if ($forwarded.length) { $message = $forwarded.children('message'); $delay = $forwarded.children('delay'); from_jid = $message.attr('from'); to_jid = $message.attr('to'); } from_bare_jid = Strophe.getBareJidFromJid(from_jid); from_resource = Strophe.getResourceFromJid(from_jid); is_me = from_bare_jid === converse.bare_jid; msgid = $message.attr('id'); if (is_me) { // I am the sender, so this must be a forwarded message... contact_jid = Strophe.getBareJidFromJid(to_jid); resource = Strophe.getResourceFromJid(to_jid); } else { contact_jid = from_bare_jid; resource = from_resource; } // Get chat box, but only create a new one when the message has a body. chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0); if (!chatbox) { return true; } if (msgid && chatbox.messages.findWhere({msgid: msgid})) { return true; // We already have this message stored. } if (chatbox.shouldPlayNotification($message)) { converse.playNotification(); } chatbox.createMessage($message, $delay, archive_id); converse.roster.addResource(contact_jid, resource); converse.emit('message', message); return true; }, getChatBox: function (jid, create) { /* Returns a chat box or optionally return a newly * created one if one doesn't exist. * * Parameters: * (String) jid - The JID of the user whose chat box we want * (Boolean) create - Should a new chat box be created if none exists? */ jid = jid.toLowerCase(); var bare_jid = Strophe.getBareJidFromJid(jid); var chatbox = this.get(bare_jid); if (!chatbox && create) { var roster_item = converse.roster.get(bare_jid); if (roster_item === undefined) { converse.log('Could not get roster item for JID '+bare_jid, 'error'); return; } chatbox = this.create({ 'id': bare_jid, 'jid': bare_jid, 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'), 'image_type': roster_item.get('image_type'), 'image': roster_item.get('image'), 'url': roster_item.get('url') }); } return chatbox; } }); this.ChatBoxViews = Backbone.Overview.extend({ initialize: function () { this.model.on("add", this.onChatBoxAdded, this); this.model.on("change:minimized", function (item) { if (item.get('minimized') === true) { /* When a chat is minimized in trimChats, trimChats needs to be * called again (in case the minimized chats toggle is newly shown). */ this.trimChats(); } else { this.trimChats(this.get(item.get('id'))); } }, this); }, _ensureElement: function () { /* Override method from backbone.js * If the #conversejs element doesn't exist, create it. */ if (!this.el) { var $el = $('#conversejs'); if (!$el.length) { $el = $('
      '); $('body').append($el); } $el.html(converse.templates.chats_panel()); this.setElement($el, false); } else { this.setElement(_.result(this, 'el'), false); } }, onChatBoxAdded: function (item) { var view = this.get(item.get('id')); if (!view) { if (item.get('chatroom')) { view = new converse.ChatRoomView({'model': item}); } else if (item.get('box_id') === 'controlbox') { view = new converse.ControlBoxView({model: item}); } else { view = new converse.ChatBoxView({model: item}); } this.add(item.get('id'), view); } else { delete view.model; // Remove ref to old model to help garbage collection view.model = item; view.initialize(); } this.trimChats(view); }, trimChats: function (newchat) { /* This method is called when a newly created chat box will * be shown. * * It checks whether there is enough space on the page to show * another chat box. Otherwise it minimize the oldest chat box * to create space. */ if (converse.no_trimming || (this.model.length <= 1)) { return; } var oldest_chat, controlbox_width = 0, $minimized = converse.minimized_chats.$el, minimized_width = _.contains(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0, boxes_width = newchat ? newchat.$el.outerWidth(true) : 0, new_id = newchat ? newchat.model.get('id') : null, controlbox = this.get('controlbox'); if (!controlbox || !controlbox.$el.is(':visible')) { controlbox_width = converse.controlboxtoggle.$el.outerWidth(true); } else { controlbox_width = controlbox.$el.outerWidth(true); } _.each(this.getAll(), function (view) { var id = view.model.get('id'); if ((id !== 'controlbox') && (id !== new_id) && (!view.model.get('minimized')) && view.$el.is(':visible')) { boxes_width += view.$el.outerWidth(true); } }); if ((minimized_width + boxes_width + controlbox_width) > $('body').outerWidth(true)) { oldest_chat = this.getOldestMaximizedChat(); if (oldest_chat && oldest_chat.get('id') !== new_id) { oldest_chat.minimize(); } } }, getOldestMaximizedChat: function () { // Get oldest view (which is not controlbox) var i = 0; var model = this.model.sort().at(i); while (model.get('id') === 'controlbox' || model.get('minimized') === true) { i++; model = this.model.at(i); if (!model) { return null; } } return model; }, closeAllChatBoxes: function (include_controlbox) { // TODO: once Backbone.Overview has been refactored, we should // be able to call .each on the views themselves. var ids = []; this.model.each(function (model) { var id = model.get('id'); if (include_controlbox || id !== 'controlbox') { ids.push(id); } }); ids.forEach(function(id) { var chatbox = this.get(id); if (chatbox) { chatbox.close(); } }, this); return this; }, showChat: function (attrs) { /* Find the chat box and show it. If it doesn't exist, create it. */ var chatbox = this.model.get(attrs.jid); if (!chatbox) { chatbox = this.model.create(attrs, { 'error': function (model, response) { converse.log(response.responseText); } }); } if (chatbox.get('minimized')) { chatbox.maximize(); } else { chatbox.trigger('show', true); } return chatbox; } }); this.MinimizedChatBoxView = Backbone.View.extend({ tagName: 'div', className: 'chat-head', events: { 'click .close-chatbox-button': 'close', 'click .restore-chat': 'restore' }, initialize: function () { this.model.messages.on('add', function (m) { if (m.get('message')) { this.updateUnreadMessagesCounter(); } }, this); this.model.on('change:minimized', this.clearUnreadMessagesCounter, this); }, render: function () { var data = _.extend( this.model.toJSON(), { 'tooltip': __('Click to restore this chat') } ); if (this.model.get('chatroom')) { data.title = this.model.get('name'); this.$el.addClass('chat-head-chatroom'); } else { data.title = this.model.get('fullname'); this.$el.addClass('chat-head-chatbox'); } return this.$el.html(converse.templates.trimmed_chat(data)); }, clearUnreadMessagesCounter: function () { this.model.set({'num_unread': 0}); this.render(); }, updateUnreadMessagesCounter: function () { this.model.set({'num_unread': this.model.get('num_unread') + 1}); this.render(); }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.remove(); this.model.destroy(); converse.emit('chatBoxClosed', this); return this; }, restore: _.debounce(function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.model.messages.off('add',null,this); this.remove(); this.model.maximize(); }, 200, true) }); this.MinimizedChats = Backbone.Overview.extend({ el: "#minimized-chats", events: { "click #toggle-minimized-chats": "toggle" }, initialize: function () { this.initToggle(); this.model.on("add", this.onChanged, this); this.model.on("destroy", this.removeChat, this); this.model.on("change:minimized", this.onChanged, this); this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this); }, tearDown: function () { this.model.off("add", this.onChanged); this.model.off("destroy", this.removeChat); this.model.off("change:minimized", this.onChanged); this.model.off('change:num_unread', this.updateUnreadMessagesCounter); return this; }, initToggle: function () { this.toggleview = new converse.MinimizedChatsToggleView({ model: new converse.MinimizedChatsToggle() }); var id = b64_sha1('converse.minchatstoggle'+converse.bare_jid); this.toggleview.model.id = id; // Appears to be necessary for backbone.browserStorage this.toggleview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id); this.toggleview.model.fetch(); }, render: function () { if (this.keys().length === 0) { this.$el.hide('fast'); } else if (this.keys().length === 1) { this.$el.show('fast'); } return this.$el; }, toggle: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')}); this.$('.minimized-chats-flyout').toggle(); }, onChanged: function (item) { if (item.get('id') !== 'controlbox' && item.get('minimized')) { this.addChat(item); } else if (this.get(item.get('id'))) { this.removeChat(item); } }, addChat: function (item) { var existing = this.get(item.get('id')); if (existing && existing.$el.parent().length !== 0) { return; } var view = new converse.MinimizedChatBoxView({model: item}); this.$('.minimized-chats-flyout').append(view.render()); this.add(item.get('id'), view); this.toggleview.model.set({'num_minimized': this.keys().length}); this.render(); }, removeChat: function (item) { this.remove(item.get('id')); this.toggleview.model.set({'num_minimized': this.keys().length}); this.render(); }, updateUnreadMessagesCounter: function () { var ls = this.model.pluck('num_unread'), count = 0, i; for (i=0; i name2? 1 : 0); } else { return converse.STATUS_WEIGHTS[status1] < converse.STATUS_WEIGHTS[status2] ? -1 : 1; } }, subscribeToSuggestedItems: function (msg) { $(msg).find('item').each(function (i, items) { if (this.getAttribute('action') === 'add') { converse.roster.addAndSubscribe( this.getAttribute('jid'), null, converse.xmppstatus.get('fullname')); } }); return true; }, isSelf: function (jid) { return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid)); }, addAndSubscribe: function (jid, name, groups, message, attributes) { /* Add a roster contact and then once we have confirmation from * the XMPP server we subscribe to that contact's presence updates. * Parameters: * (String) jid - The Jabber ID of the user being added and subscribed to. * (String) name - The name of that user * (Array of Strings) groups - Any roster groups the user might belong to * (String) message - An optional message to explain the * reason for the subscription request. * (Object) attributes - Any additional attributes to be stored on the user's model. */ this.addContact(jid, name, groups, attributes).done(function (contact) { if (contact instanceof converse.RosterContact) { contact.subscribe(message); } }); }, sendContactAddIQ: function (jid, name, groups, callback, errback) { /* Send an IQ stanza to the XMPP server to add a new roster contact. * Parameters: * (String) jid - The Jabber ID of the user being added * (String) name - The name of that user * (Array of Strings) groups - Any roster groups the user might belong to * (Function) callback - A function to call once the VCard is returned * (Function) errback - A function to call if an error occured */ name = _.isEmpty(name)? jid: name; var iq = $iq({type: 'set'}) .c('query', {xmlns: Strophe.NS.ROSTER}) .c('item', { jid: jid, name: name }); _.map(groups, function (group) { iq.c('group').t(group).up(); }); converse.connection.sendIQ(iq, callback, errback); }, addContact: function (jid, name, groups, attributes) { /* Adds a RosterContact instance to converse.roster and * registers the contact on the XMPP server. * Returns a promise which is resolved once the XMPP server has * responded. * Parameters: * (String) jid - The Jabber ID of the user being added and subscribed to. * (String) name - The name of that user * (Array of Strings) groups - Any roster groups the user might belong to * (Object) attributes - Any additional attributes to be stored on the user's model. */ var deferred = new $.Deferred(); groups = groups || []; name = _.isEmpty(name)? jid: name; this.sendContactAddIQ(jid, name, groups, function (iq) { var contact = this.create(_.extend({ ask: undefined, fullname: name, groups: groups, jid: jid, requesting: false, subscription: 'none' }, attributes), {sort: false}); deferred.resolve(contact); }.bind(this), function (err) { alert(__("Sorry, there was an error while trying to add "+name+" as a contact.")); converse.log(err); deferred.resolve(err); } ); return deferred.promise(); }, addResource: function (bare_jid, resource) { var item = this.get(bare_jid), resources; if (item) { resources = item.get('resources'); if (resources) { if (_.indexOf(resources, resource) === -1) { resources.push(resource); item.set({'resources': resources}); } } else { item.set({'resources': [resource]}); } } }, subscribeBack: function (bare_jid) { var contact = this.get(bare_jid); if (contact instanceof converse.RosterContact) { contact.authorize().subscribe(); } else { // Can happen when a subscription is retried or roster was deleted this.addContact(bare_jid, '', [], { 'subscription': 'from' }).done(function (contact) { if (contact instanceof converse.RosterContact) { contact.authorize().subscribe(); } }); } }, getNumOnlineContacts: function () { var count = 0, ignored = ['offline', 'unavailable'], models = this.models, models_length = models.length, i; if (converse.show_only_online_users) { ignored = _.union(ignored, ['dnd', 'xa', 'away']); } for (i=0; i 0) { this.$el.show(); } }, toggle: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var $el = $(ev.target); if ($el.hasClass("icon-opened")) { this.$el.nextUntil('dt').slideUp(); this.model.save({state: CLOSED}); $el.removeClass("icon-opened").addClass("icon-closed"); } else { $el.removeClass("icon-closed").addClass("icon-opened"); this.model.save({state: OPENED}); this.filter( converse.rosterview.$('.roster-filter').val(), converse.rosterview.$('.filter-type').val() ); } }, onContactGroupChange: function (contact) { var in_this_group = _.contains(contact.get('groups'), this.model.get('name')); var cid = contact.get('id'); var in_this_overview = !this.get(cid); if (in_this_group && !in_this_overview) { this.model.contacts.remove(cid); } else if (!in_this_group && in_this_overview) { this.addContact(contact); } }, onContactSubscriptionChange: function (contact) { if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') { this.model.contacts.remove(contact.get('id')); } }, onContactRequestChange: function (contact) { if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) { this.model.contacts.remove(contact.get('id')); } }, onRemove: function (contact) { this.remove(contact.get('id')); if (this.model.contacts.length === 0) { this.$el.hide(); } } }); this.RosterGroups = Backbone.Collection.extend({ model: converse.RosterGroup, comparator: function (a, b) { /* Groups are sorted alphabetically, ignoring case. * However, Ungrouped, Requesting Contacts and Pending Contacts * appear last and in that order. */ a = a.get('name'); b = b.get('name'); var special_groups = _.keys(HEADER_WEIGHTS); var a_is_special = _.contains(special_groups, a); var b_is_special = _.contains(special_groups, b); if (!a_is_special && !b_is_special ) { return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0); } else if (a_is_special && b_is_special) { return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0); } else if (!a_is_special && b_is_special) { return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1; } else if (a_is_special && !b_is_special) { return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1; } } }); this.RosterView = Backbone.Overview.extend({ tagName: 'div', id: 'converse-roster', events: { "keydown .roster-filter": "liveFilter", "click .onX": "clearFilter", "mousemove .x": "togglePointer", "change .filter-type": "changeFilterType" }, initialize: function () { this.roster_handler_ref = this.registerRosterHandler(); this.rosterx_handler_ref = this.registerRosterXHandler(); this.presence_ref = this.registerPresenceHandler(); converse.roster.on("add", this.onContactAdd, this); converse.roster.on('change', this.onContactChange, this); converse.roster.on("destroy", this.update, this); converse.roster.on("remove", this.update, this); this.model.on("add", this.onGroupAdd, this); this.model.on("reset", this.reset, this); this.$roster = $(''); }, unregisterHandlers: function () { converse.connection.deleteHandler(this.roster_handler_ref); delete this.roster_handler_ref; converse.connection.deleteHandler(this.rosterx_handler_ref); delete this.rosterx_handler_ref; converse.connection.deleteHandler(this.presence_ref); delete this.presence_ref; }, update: _.debounce(function () { var $count = $('#online-count'); $count.text('('+converse.roster.getNumOnlineContacts()+')'); if (!$count.is(':visible')) { $count.show(); } if (this.$roster.parent().length === 0) { this.$el.append(this.$roster.show()); } return this.showHideFilter(); }, converse.animate ? 100 : 0), render: function () { this.$el.html(converse.templates.roster({ placeholder: __('Type to filter'), label_contacts: LABEL_CONTACTS, label_groups: LABEL_GROUPS })); if (!converse.allow_contact_requests) { // XXX: if we ever support live editing of config then // we'll need to be able to remove this class on the fly. this.$el.addClass('no-contact-requests'); } return this; }, fetch: function () { this.model.fetch({ silent: true, // We use the success handler to handle groups that were added, // we need to first have all groups before positionFetchedGroups // will work properly. success: function (collection, resp, options) { if (collection.length !== 0) { this.positionFetchedGroups(collection, resp, options); } converse.roster.fetch({ add: true, success: function (collection) { if (collection.length === 0) { /* We don't have any roster contacts stored in sessionStorage, * so lets fetch the roster from the XMPP server. We pass in * 'sendPresence' as callback method, because after initially * fetching the roster we are ready to receive presence * updates from our contacts. */ converse.roster.fetchFromServer(function () { converse.xmppstatus.sendPresence(); }); } else if (converse.send_initial_presence) { /* We're not going to fetch the roster again because we have * it already cached in sessionStorage, but we still need to * send out a presence stanza because this is a new session. * See: https://github.com/jcbrand/converse.js/issues/536 */ converse.xmppstatus.sendPresence(); } } }); }.bind(this) }); return this; }, changeFilterType: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.clearFilter(); this.filter( this.$('.roster-filter').val(), ev.target.value ); }, tog: function (v) { return v?'addClass':'removeClass'; }, togglePointer: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var el = ev.target; $(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX'); }, filter: function (query, type) { query = query.toLowerCase(); if (type === 'groups') { _.each(this.getAll(), function (view, idx) { if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) { view.hide(); } else if (view.model.contacts.length > 0) { view.show(); } }); } else { _.each(this.getAll(), function (view) { view.filter(query, type); }); } }, liveFilter: _.debounce(function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var $filter = this.$('.roster-filter'); var q = $filter.val(); var t = this.$('.filter-type').val(); $filter[this.tog(q)]('x'); this.filter(q, t); }, 300), clearFilter: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); $(ev.target).removeClass('x onX').val(''); } this.filter(''); }, showHideFilter: function () { if (!this.$el.is(':visible')) { return; } var $filter = this.$('.roster-filter'); var $type = this.$('.filter-type'); var visible = $filter.is(':visible'); if (visible && $filter.val().length > 0) { // Don't hide if user is currently filtering. return; } if (this.$roster.hasScrollBar()) { if (!visible) { $filter.show(); $type.show(); } } else { $filter.hide(); $type.hide(); } return this; }, reset: function () { converse.roster.reset(); this.removeAll(); this.$roster = $(''); this.render().update(); return this; }, registerRosterHandler: function () { converse.connection.addHandler( converse.roster.onRosterPush.bind(converse.roster), Strophe.NS.ROSTER, 'iq', "set" ); }, registerRosterXHandler: function () { var t = 0; converse.connection.addHandler( function (msg) { window.setTimeout( function () { converse.connection.flush(); converse.roster.subscribeToSuggestedItems.bind(converse.roster)(msg); }, t ); t += $(msg).find('item').length*250; return true; }, Strophe.NS.ROSTERX, 'message', null ); }, registerPresenceHandler: function () { converse.connection.addHandler( function (presence) { converse.roster.presenceHandler(presence); return true; }.bind(this), null, 'presence', null); }, onGroupAdd: function (group) { var view = new converse.RosterGroupView({model: group}); this.add(group.get('name'), view.render()); this.positionGroup(view); }, onContactAdd: function (contact) { this.addRosterContact(contact).update(); if (!contact.get('vcard_updated')) { // This will update the vcard, which triggers a change // request which will rerender the roster contact. converse.getVCard(contact.get('jid')); } }, onContactChange: function (contact) { this.updateChatBox(contact).update(); if (_.has(contact.changed, 'subscription')) { if (contact.changed.subscription === 'from') { this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); } else if (_.contains(['both', 'to'], contact.get('subscription'))) { this.addExistingContact(contact); } } if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') { this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); } if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') { this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); } this.liveFilter(); }, updateChatBox: function (contact) { var chatbox = converse.chatboxes.get(contact.get('jid')), changes = {}; if (!chatbox) { return this; } if (_.has(contact.changed, 'chat_status')) { changes.chat_status = contact.get('chat_status'); } if (_.has(contact.changed, 'status')) { changes.status = contact.get('status'); } chatbox.save(changes); return this; }, positionFetchedGroups: function (model, resp, options) { /* Instead of throwing an add event for each group * fetched, we wait until they're all fetched and then * we position them. * Works around the problem of positionGroup not * working when all groups besides the one being * positioned aren't already in inserted into the * roster DOM element. */ model.sort(); model.each(function (group, idx) { var view = this.get(group.get('name')); if (!view) { view = new converse.RosterGroupView({model: group}); this.add(group.get('name'), view.render()); } if (idx === 0) { this.$roster.append(view.$el); } else { this.appendGroup(view); } }.bind(this)); }, positionGroup: function (view) { /* Place the group's DOM element in the correct alphabetical * position amongst the other groups in the roster. */ var $groups = this.$roster.find('.roster-group'), index = $groups.length ? this.model.indexOf(view.model) : 0; if (index === 0) { this.$roster.prepend(view.$el); } else if (index === (this.model.length-1)) { this.appendGroup(view); } else { $($groups.eq(index)).before(view.$el); } return this; }, appendGroup: function (view) { /* Add the group at the bottom of the roster */ var $last = this.$roster.find('.roster-group').last(); var $siblings = $last.siblings('dd'); if ($siblings.length > 0) { $siblings.last().after(view.$el); } else { $last.after(view.$el); } return this; }, getGroup: function (name) { /* Returns the group as specified by name. * Creates the group if it doesn't exist. */ var view = this.get(name); if (view) { return view.model; } return this.model.create({name: name, id: b64_sha1(name)}); }, addContactToGroup: function (contact, name) { this.getGroup(name).contacts.add(contact); }, addExistingContact: function (contact) { var groups; if (converse.roster_groups) { groups = contact.get('groups'); if (groups.length === 0) { groups = [HEADER_UNGROUPED]; } } else { groups = [HEADER_CURRENT_CONTACTS]; } _.each(groups, _.bind(this.addContactToGroup, this, contact)); }, addRosterContact: function (contact) { if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') { this.addExistingContact(contact); } else { if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) { this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); } else if (contact.get('requesting') === true) { this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); } } return this; } }); this.XMPPStatus = Backbone.Model.extend({ initialize: function () { this.set({ 'status' : this.getStatus() }); this.on('change', function (item) { if (this.get('fullname') === undefined) { converse.getVCard( null, // No 'to' attr when getting one's own vCard function (iq, jid, fullname, image, image_type, url) { this.save({'fullname': fullname}); }.bind(this) ); } if (_.has(item.changed, 'status')) { converse.emit('statusChanged', this.get('status')); } if (_.has(item.changed, 'status_message')) { converse.emit('statusMessageChanged', this.get('status_message')); } }.bind(this)); }, constructPresence: function (type, status_message) { if (typeof type === 'undefined') { type = this.get('status') || 'online'; } if (typeof status_message === 'undefined') { status_message = this.get('status_message'); } var presence; // Most of these presence types are actually not explicitly sent, // but I add all of them here fore reference and future proofing. if ((type === 'unavailable') || (type === 'probe') || (type === 'error') || (type === 'unsubscribe') || (type === 'unsubscribed') || (type === 'subscribe') || (type === 'subscribed')) { presence = $pres({'type': type}); } else if (type === 'offline') { presence = $pres({'type': 'unavailable'}); if (status_message) { presence.c('show').t(type); } } else { if (type === 'online') { presence = $pres(); } else { presence = $pres().c('show').t(type).up(); } if (status_message) { presence.c('status').t(status_message); } } return presence; }, sendPresence: function (type, status_message) { converse.connection.send(this.constructPresence(type, status_message)); }, setStatus: function (value) { this.sendPresence(value); this.save({'status': value}); }, getStatus: function () { return this.get('status') || 'online'; }, setStatusMessage: function (status_message) { this.sendPresence(this.getStatus(), status_message); var prev_status = this.get('status_message'); this.save({'status_message': status_message}); if (this.xhr_custom_status) { $.ajax({ url: this.xhr_custom_status_url, type: 'POST', data: {'msg': status_message} }); } if (prev_status === status_message) { this.trigger("update-status-ui", this); } } }); this.XMPPStatusView = Backbone.View.extend({ el: "span#xmpp-status-holder", events: { "click a.choose-xmpp-status": "toggleOptions", "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm", "submit #set-custom-xmpp-status": "setStatusMessage", "click .dropdown dd ul li a": "setStatus" }, initialize: function () { this.model.on("change:status", this.updateStatusUI, this); this.model.on("change:status_message", this.updateStatusUI, this); this.model.on("update-status-ui", this.updateStatusUI, this); }, render: function () { // Replace the default dropdown with something nicer var $select = this.$el.find('select#select-xmpp-status'), chat_status = this.model.get('status') || 'offline', options = $('option', $select), $options_target, options_list = []; this.$el.html(converse.templates.choose_status()); this.$el.find('#fancy-xmpp-status-select') .html(converse.templates.chat_status({ 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)), 'chat_status': chat_status, 'desc_custom_status': __('Click here to write a custom status message'), 'desc_change_status': __('Click to change your chat status') })); // iterate through all the