From ba05d4e77927351a10e9286668d1bf685dfdc7e7 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 26 Feb 2016 17:28:18 +0000 Subject: [PATCH] Add a build step to create a build of only the converse.js modules This is now used in non_amd.html and means that the individual converse.js modules don't need to support the non-AMD case. --- .gitignore | 3 + Gruntfile.js | 2 + Makefile | 10 +- builds/converse-no-dependencies.js | 10847 +++++++ builds/converse.js | 43903 --------------------------- builds/templates.js | 11 + non_amd.html | 21 +- src/build-no-dependencies.js | 25 + src/build-no-jquery.js | 7 +- src/jquery-external.js | 4 - src/jquery-private-external.js | 3 - src/wrapper-end.js | 30 + 12 files changed, 10938 insertions(+), 43928 deletions(-) create mode 100644 builds/converse-no-dependencies.js delete mode 100644 builds/converse.js create mode 100644 src/build-no-dependencies.js delete mode 100644 src/jquery-external.js delete mode 100644 src/jquery-private-external.js create mode 100644 src/wrapper-end.js diff --git a/.gitignore b/.gitignore index 8b6664e5e..b12795f39 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ develop-eggs .DS_Store # Builds +builds/converse-no-dependencies.min.js builds/converse-no-locales-no-otr.js builds/converse-no-locales-no-otr.min.js builds/converse-no-otr.js @@ -52,3 +53,5 @@ builds/converse.nojquery.js builds/converse.nojquery.min.js css/converse.css.map + +.sv? diff --git a/Gruntfile.js b/Gruntfile.js index 27a520ab7..9c291d567 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -60,6 +60,8 @@ module.exports = function(grunt) { rjs + ' -o src/build.js optimize=none out=builds/converse.js && ' + rjs + ' -o src/build-no-jquery.js &&' + rjs + ' -o src/build-no-jquery.js optimize=none out=builds/converse.nojquery.js && ' + + rjs + ' -o src/build-no-dependencies.js &&' + + rjs + ' -o src/build-no-dependencies.js optimize=none out=builds/converse-no-dependencies.js && ' + rjs + ' -o src/build-no-locales-no-otr.js && ' + rjs + ' -o src/build-no-locales-no-otr.js optimize=none out=builds/converse-no-locales-no-otr.js', callback); // XXX: It might be possible to not have separate build config files. For example: diff --git a/Makefile b/Makefile index 50ea11d67..10dba336f 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ HTTPSERVE ?= ./node_modules/.bin/http-server JSHINT ?= ./node_modules/.bin/jshint PAPER = PHANTOMJS ?= ./node_modules/.bin/phantomjs +RJS ?= ./node_modules/.bin/r.js PO2JSON ?= ./node_modules/.bin/po2json SASS ?= ./.bundle/bin/sass SPHINXBUILD ?= ./bin/sphinx-build @@ -119,7 +120,7 @@ clean:: rm -rf node_modules components .bundle .PHONY: dev -dev: stamp-bower stamp-bundler +dev: stamp-bower stamp-bundler converse ######################################################################## ## Builds @@ -142,6 +143,11 @@ jsmin: cssmin: stamp-npm $(GRUNT) cssmin +.PHONY: converse +converse:: stamp-npm + $(RJS) -o src/build-no-dependencies.js + $(RJS) -o src/build-no-dependencies.js optimize=none out=builds/converse-no-dependencies.js + .PHONY: build build:: stamp-npm $(GRUNT) jst @@ -168,7 +174,7 @@ html: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -.PHONY: doc +.PHONY: html doc: html .PHONY: epub diff --git a/builds/converse-no-dependencies.js b/builds/converse-no-dependencies.js new file mode 100644 index 000000000..b6b4e6107 --- /dev/null +++ b/builds/converse-no-dependencies.js @@ -0,0 +1,10847 @@ +/** + * @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