Source: jsxc.lib.js

/**
 * JavaScript Xmpp Chat namespace
 *
 * @namespace jsxc
 */
jsxc = {
   /** Version of jsxc */
   version: '< $ app.version $ >',

   /** True if i'm the master */
   master: false,

   /** True if the role allocation is finished */
   role_allocation: false,

   /** Timeout for keepalive */
   to: [],

   /** Timeout after normal keepalive starts */
   toBusy: null,

   /** Timeout for notification */
   toNotification: null,

   /** Timeout delay for notification */
   toNotificationDelay: 500,

   /** Interval for keep-alive */
   keepaliveInterval: null,

   /** True if restore is complete */
   restoreCompleted: false,

   /** True if login through box */
   triggeredFromBox: false,

   /** True if logout through element click */
   triggeredFromElement: false,

   /** True if logout through logout click */
   triggeredFromLogout: false,

   /** last values which we wrote into localstorage (IE workaround) */
   ls: [],

   /**
    * storage event is even fired if I write something into storage (IE
    * workaround) 0: conform, 1: not conform, 2: not shure
    */
   storageNotConform: null,

   /** Timeout for storageNotConform test */
   toSNC: null,

   /** My bar id */
   bid: null,

   /** Current state */
   currentState: null,

   /** Current UI state */
   currentUIState: null,

   /** Some constants */
   CONST: {
      NOTIFICATION_DEFAULT: 'default',
      NOTIFICATION_GRANTED: 'granted',
      NOTIFICATION_DENIED: 'denied',
      STATUS: ['offline', 'dnd', 'xa', 'away', 'chat', 'online'],
      SOUNDS: {
         MSG: 'incomingMessage.wav',
         CALL: 'Rotary-Phone6.mp3',
         NOTICE: 'Ping1.mp3'
      },
      REGEX: {
         JID: new RegExp('\\b[^"&\'\\/:<>@\\s]+@[\\w-_.]+\\b', 'ig'),
         URL: new RegExp(/(https?:\/\/|www\.)[^\s<>'"]+/gi)
      },
      NS: {
         CARBONS: 'urn:xmpp:carbons:2',
         FORWARD: 'urn:xmpp:forward:0',
         HINTS: 'urn:xmpp:hints'
      },
      HIDDEN: 'hidden',
      SHOWN: 'shown',
      STATE: {
         INITIATING: 0,
         PREVCONFOUND: 1,
         SUSPEND: 2,
         TRYTOINTERCEPT: 3,
         INTERCEPTED: 4,
         ESTABLISHING: 5,
         READY: 6
      },
      UISTATE: {
         INITIATING: 0,
         READY: 1
      }
   },

   /**
    * Parse a unix timestamp and return a formatted time string
    *
    * @memberOf jsxc
    * @param {Object} unixtime
    * @returns time of day and/or date
    */
   getFormattedTime: function(unixtime) {
      var msgDate = new Date(parseInt(unixtime));
      var day = ('0' + msgDate.getDate()).slice(-2);
      var month = ('0' + (msgDate.getMonth() + 1)).slice(-2);
      var year = msgDate.getFullYear();
      var hours = ('0' + msgDate.getHours()).slice(-2);
      var minutes = ('0' + msgDate.getMinutes()).slice(-2);
      var dateNow = new Date();

      var date = (typeof msgDate.toLocaleDateString === 'function') ? msgDate.toLocaleDateString() : day + '.' + month + '.' + year;
      var time = (typeof msgDate.toLocaleTimeString === 'function') ? msgDate.toLocaleTimeString() : hours + ':' + minutes;

      // compare dates only
      dateNow.setHours(0, 0, 0, 0);
      msgDate.setHours(0, 0, 0, 0);

      if (dateNow.getTime() !== msgDate.getTime()) {
         return date + ' ' + time;
      }
      return time;
   },

   /**
    * Write debug message to console and to log.
    *
    * @memberOf jsxc
    * @param {String} msg Debug message
    * @param {Object} data
    * @param {String} Could be warn|error|null
    */
   debug: function(msg, data, level) {
      if (level) {
         msg = '[' + level + '] ' + msg;
      }

      if (data) {
         if (jsxc.storage.getItem('debug') === true) {
            console.log(msg, data);
         }

         // try to convert data to string
         var d;
         try {
            // clone html snippet
            d = $("<span>").prepend($(data).clone()).html();
         } catch (err) {
            try {
               d = JSON.stringify(data);
            } catch (err2) {
               d = 'see js console';
            }
         }

         jsxc.log = jsxc.log + '$ ' + msg + ': ' + d + '\n';
      } else {
         console.log(msg);
         jsxc.log = jsxc.log + '$ ' + msg + '\n';
      }
   },

   /**
    * Write warn message.
    *
    * @memberOf jsxc
    * @param {String} msg Warn message
    * @param {Object} data
    */
   warn: function(msg, data) {
      jsxc.debug(msg, data, 'WARN');
   },

   /**
    * Write error message.
    *
    * @memberOf jsxc
    * @param {String} msg Error message
    * @param {Object} data
    */
   error: function(msg, data) {
      jsxc.debug(msg, data, 'ERROR');
   },

   /** debug log */
   log: '',

   /**
    * This function initializes important core functions and event handlers.
    * Afterwards it performs the following actions in the given order:
    *
    * <ol>
    *  <li>If (loginForm.ifFound = 'force' and form was found) or (jid or rid or
    * 	sid was not found) intercept form, and listen for credentials.</li>
    *  <li>Attach with jid, rid and sid from storage, if no form was found or
    * 	loginForm.ifFound = 'attach'</li>
    *  <li>Attach with jid, rid and sid from options.xmpp, if no form was found or
    * 	loginForm.ifFound = 'attach'</li>
    * </ol>
    *
    * @memberOf jsxc
    * @param {object} options See {@link jsxc.options}
    */
   init: function(options) {
      jsxc.changeState(jsxc.CONST.STATE.INITIATING);

      if (options && options.loginForm && typeof options.loginForm.attachIfFound === 'boolean' && !options.loginForm.ifFound) {
         // translate deprated option attachIfFound found to new ifFound
         options.loginForm.ifFound = (options.loginForm.attachIfFound) ? 'attach' : 'pause';
      }

      if (options) {
         // override default options
         $.extend(true, jsxc.options, options);
      }

      // Check localStorage
      if (!jsxc.storage.hasSupport()) {
         jsxc.error("Browser doesn't support localStorage. JSXC will be disabled.");
         return;
      }

      /**
       * Getter method for options. Saved options will override default one.
       *
       * @param {string} key option key
       * @returns default or saved option value
       */
      jsxc.options.get = function(key) {
         if (jsxc.bid) {
            var local = jsxc.storage.getUserItem('options') || {};

            return (typeof local[key] !== 'undefined') ? local[key] : jsxc.options[key];
         }

         return jsxc.options[key];
      };

      /**
       * Setter method for options. Will write into localstorage.
       *
       * @param {string} key option key
       * @param {object} value option value
       */
      jsxc.options.set = function(key, value) {
         jsxc.storage.updateItem('options', key, value, true);
      };

      jsxc.storageNotConform = jsxc.storage.getItem('storageNotConform');
      if (jsxc.storageNotConform === null) {
         jsxc.storageNotConform = 2;
      }

      // detect language
      var lang;
      if (jsxc.storage.getItem('lang') !== null) {
         lang = jsxc.storage.getItem('lang');
      } else if (jsxc.options.autoLang && navigator.languages && navigator.languages.length > 0) {
         lang = navigator.languages[0].substr(0, 2);
      } else if (jsxc.options.autoLang && navigator.language) {
         lang = navigator.language.substr(0, 2);
      } else {
         lang = jsxc.options.defaultLang;
      }

      // initialize i18next translator
      window.i18next.init({
         lng: lang,
         fallbackLng: 'en',
         resources: I18next,
         returnNull: false,
         debug: jsxc.storage.getItem('debug') === true,
         interpolation: {
            prefix: '__',
            suffix: '__'
         }
      }, function() {
         window.jqueryI18next.init(window.i18next, $, {
            tName: 't',
            i18nName: 'i18next',
            handleName: 'localize',
         });
      });

      if (jsxc.storage.getItem('debug') === true) {
         jsxc.options.otr.debug = true;
      }

      // Register event listener for the storage event
      window.addEventListener('storage', jsxc.storage.onStorage, false);

      $(document).on('attached.jsxc', jsxc.registerLogout);

      var isStorageAttachParameters = jsxc.storage.getItem('rid') && jsxc.storage.getItem('sid') && jsxc.storage.getItem('jid');
      var isOptionsAttachParameters = jsxc.options.xmpp.rid && jsxc.options.xmpp.sid && jsxc.options.xmpp.jid;
      var isForceLoginForm = jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'force' && jsxc.isLoginForm();

      // Check if we have to establish a new connection
      if ((!isStorageAttachParameters && !isOptionsAttachParameters) || isForceLoginForm) {

         // clean up rid and sid
         jsxc.storage.removeItem('rid');
         jsxc.storage.removeItem('sid');

         // Looking for a login form
         if (!jsxc.isLoginForm()) {
            jsxc.changeState(jsxc.CONST.STATE.SUSPEND);

            if (jsxc.options.displayRosterMinimized()) {
               // Show minimized roster
               jsxc.storage.setUserItem('roster', 'hidden');
               jsxc.gui.roster.init();
               jsxc.gui.roster.noConnection();
            }

            return;
         }

         jsxc.changeState(jsxc.CONST.STATE.TRYTOINTERCEPT);

         if (typeof jsxc.options.formFound === 'function') {
            jsxc.options.formFound.call();
         }

         // create jquery object
         var form = jsxc.options.loginForm.form = $(jsxc.options.loginForm.form);
         var events = form.data('events') || {
            submit: []
         };
         var submits = [];

         // save attached submit events and remove them. Will be reattached
         // in jsxc.submitLoginForm
         $.each(events.submit, function(index, val) {
            submits.push(val.handler);
         });

         form.data('submits', submits);
         form.off('submit');

         // Add jsxc login action to form
         form.submit(function(ev) {
            ev.preventDefault();

            jsxc.prepareLogin(function(settings) {
               if (settings !== false) {
                  // settings.xmpp.onlogin is deprecated since v2.1.0
                  var enabled = (settings.loginForm && settings.loginForm.enable) || (settings.xmpp && settings.xmpp.onlogin);
                  enabled = enabled === "true" || enabled === true;

                  if (enabled) {
                     jsxc.options.loginForm.triggered = true;

                     jsxc.xmpp.login(jsxc.options.xmpp.jid, jsxc.options.xmpp.password);

                     return;
                  }
               }

               jsxc.submitLoginForm();
            });

            // Trigger submit in jsxc.xmpp.connected()
            return false;
         });

         jsxc.changeState(jsxc.CONST.STATE.INTERCEPTED);

      } else if (!jsxc.isLoginForm() || (jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'attach')) {

         // Restore old connection
         jsxc.changeState(jsxc.CONST.STATE.PREVCONFOUND);

         if (typeof jsxc.storage.getItem('alive') === 'undefined') {
            jsxc.onMaster();
         } else {
            jsxc.checkMaster();
         }
      }
   },

   /**
    * Attach to previous session if jid, sid and rid are available
    * in storage or options (default behaviour also for {@link jsxc.init}).
    *
    * @memberOf jsxc
    */
   /**
    * Start new chat session with given jid and password.
    *
    * @memberOf jsxc
    * @param {string} jid Jabber Id
    * @param {string} password Jabber password
    */
   /**
    * Attach to new chat session with jid, sid and rid.
    *
    * @memberOf jsxc
    * @param {string} jid Jabber Id
    * @param {string} sid Session Id
    * @param {string} rid Request Id
    */
   start: function() {
      var args = arguments;

      if (jsxc.role_allocation && !jsxc.master) {
         jsxc.debug('There is an other master tab');

         return false;
      }

      if (jsxc.xmpp.conn && jsxc.xmpp.connected) {
         jsxc.debug('We are already connected');

         return false;
      }

      if (args.length === 3) {
         $(document).one('attached.jsxc', function() {
            // save rid after first attachment
            jsxc.xmpp.onRidChange(jsxc.xmpp.conn._proto.rid);

            jsxc.onMaster();
         });
      }

      jsxc.checkMaster(function() {
         jsxc.xmpp.login.apply(this, args);
      });
   },

   registerLogout: function() {
      // Looking for logout element
      if (jsxc.options.logoutElement !== null && $(jsxc.options.logoutElement).length > 0) {
         var logout = function(ev) {
            ev.stopPropagation();
            ev.preventDefault();

            jsxc.options.logoutElement = $(this);
            jsxc.triggeredFromLogout = true;

            jsxc.xmpp.logout();
         };

         jsxc.options.logoutElement = $(jsxc.options.logoutElement);

         jsxc.options.logoutElement.off('click', null, logout).one('click', logout);
      }
   },

   /**
    * Returns true if login form is found.
    *
    * @memberOf jsxc
    * @returns {boolean} True if login form was found.
    */
   isLoginForm: function() {
      return jsxc.options.loginForm.form && jsxc.el_exists(jsxc.options.loginForm.form) && jsxc.el_exists(jsxc.options.loginForm.jid) && jsxc.el_exists(jsxc.options.loginForm.pass);
   },

   /**
    * Load settings and prepare jid.
    *
    * @memberOf jsxc
    * @param {string} username
    * @param {string} password
    * @param {function} cb Called after login is prepared with result as param
    */
   prepareLogin: function(username, password, cb) {
      if (typeof username === 'function') {
         cb = username;
         username = null;
      }
      username = username || $(jsxc.options.loginForm.jid).val();
      password = password || $(jsxc.options.loginForm.pass).val();

      if (!jsxc.triggeredFromBox && (jsxc.options.loginForm.onConnecting === 'dialog' || typeof jsxc.options.loginForm.onConnecting === 'undefined')) {
         jsxc.gui.showWaitAlert($.t('Logging_in'));
      }

      var settings;

      if (typeof jsxc.options.loadSettings === 'function') {
         settings = jsxc.options.loadSettings.call(this, username, password, function(s) {
            jsxc._prepareLogin(username, password, cb, s);
         });

         if (typeof settings !== 'undefined') {
            jsxc._prepareLogin(username, password, cb, settings);
         }
      } else {
         jsxc._prepareLogin(username, password, cb);
      }
   },

   /**
    * Process xmpp settings and save loaded settings.
    *
    * @private
    * @memberOf jsxc
    * @param {string} username
    * @param {string} password
    * @param {function} cb Called after login is prepared with result as param
    * @param {object} [loadedSettings] additonal options
    */
   _prepareLogin: function(username, password, cb, loadedSettings) {
      if (loadedSettings === false) {
         jsxc.warn('No settings provided');

         cb(false);
         return;
      }

      // prevent to modify the original object
      var settings = $.extend(true, {}, jsxc.options);

      if (loadedSettings) {
         // overwrite current options with loaded settings;
         settings = $.extend(true, settings, loadedSettings);
      } else {
         loadedSettings = {};
      }

      if (typeof settings.xmpp.username === 'string') {
         username = settings.xmpp.username;
      }

      if (typeof settings.xmpp.password === 'string') {
         password = settings.xmpp.password;
      }

      var resource = (settings.xmpp.resource) ? '/' + settings.xmpp.resource : '';
      var domain = settings.xmpp.domain;
      var jid;

      if (username.match(/@(.*)$/)) {
         jid = (username.match(/\/(.*)$/)) ? username : username + resource;
      } else {
         jid = username + '@' + domain + resource;
      }

      if (typeof jsxc.options.loginForm.preJid === 'function') {
         jid = jsxc.options.loginForm.preJid(jid);
      }

      jsxc.bid = jsxc.jidToBid(jid);

      settings.xmpp.username = jid.split('@')[0];
      settings.xmpp.domain = jid.split('@')[1].split('/')[0];
      settings.xmpp.resource = jid.split('@')[1].split('/')[1] || "";

      if (!loadedSettings.xmpp) {
         // force xmpp settings to be saved to storage
         loadedSettings.xmpp = {};
      }

      // save loaded settings to storage
      $.each(loadedSettings, function(key) {
         var old = jsxc.options.get(key);
         var val = settings[key];
         val = $.extend(true, old, val);

         jsxc.options.set(key, val);
      });

      jsxc.options.xmpp.jid = jid;
      jsxc.options.xmpp.password = password;

      cb(settings);
   },

   /**
    * Called if the script is a slave
    */
   onSlave: function() {
      jsxc.debug('I am the slave.');

      jsxc.role_allocation = true;
      jsxc.bid = jsxc.jidToBid(jsxc.storage.getItem('jid'));

      jsxc.gui.init();
      $('#jsxc_roster').removeClass('jsxc_noConnection');

      jsxc.registerLogout();
      jsxc.gui.avatar.update($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own');

      jsxc.gui.restore();
   },

   /**
    * Called if the script is the master
    */
   onMaster: function() {
      jsxc.debug('I am master.');

      jsxc.master = true;

      // Init local storage
      jsxc.storage.setItem('alive', 0);
      jsxc.storage.setItem('alive_busy', 0);

      // Sending keepalive signal
      jsxc.startKeepAlive();

      jsxc.role_allocation = true;

      jsxc.xmpp.login();
   },

   /**
    * Checks if there is a master
    *
    * @param {function} [cb] Called if no master was found.
    */
   checkMaster: function(cb) {
      jsxc.debug('check master');

      cb = (cb && typeof cb === 'function') ? cb : jsxc.onMaster;

      if (typeof jsxc.storage.getItem('alive') === 'undefined') {
         cb.call();
      } else {
         jsxc.to.push(window.setTimeout(cb, 1000));
         jsxc.keepAlive('slave');
      }
   },

   masterActions: function() {

      if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) {
         return;
      }

      //prepare notifications
      var noti = jsxc.storage.getUserItem('notification');
      noti = (typeof noti === 'number') ? noti : 2;
      if (jsxc.options.notification && noti > 0 && jsxc.notification.hasSupport()) {
         if (jsxc.notification.hasPermission()) {
            jsxc.notification.init();
         } else {
            jsxc.notification.prepareRequest();
         }
      } else {
         // No support => disable
         jsxc.options.notification = false;
      }

      if (jsxc.options.get('otr').enable) {
         // create or load DSA key
         jsxc.otr.createDSA();
      }

      jsxc.gui.avatar.update($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own');
   },

   /**
    * Start sending keep-alive signal
    */
   startKeepAlive: function() {
      jsxc.keepaliveInterval = window.setInterval(jsxc.keepAlive, jsxc.options.timeout - 1000);
   },

   /**
    * Sends the keep-alive signal to signal that the master is still there.
    */
   keepAlive: function(role) {
      var next = parseInt(jsxc.storage.getItem('alive')) + 1;
      role = role || 'master';

      jsxc.storage.setItem('alive', next + ':' + role);
   },

   /**
    * Send one keep-alive signal with higher timeout, and than resume with
    * normal signal
    */
   keepBusyAlive: function() {
      if (jsxc.toBusy) {
         window.clearTimeout(jsxc.toBusy);
      }

      if (jsxc.keepaliveInterval) {
         window.clearInterval(jsxc.keepaliveInterval);
      }

      jsxc.storage.ink('alive_busy');
      jsxc.toBusy = window.setTimeout(jsxc.startKeepAlive, jsxc.options.busyTimeout - 1000);
   },

   /**
    * Generates a random integer number between 0 and max
    *
    * @param {Integer} max
    * @return {Integer} random integer between 0 and max
    */
   random: function(max) {
      return Math.floor(Math.random() * max);
   },

   /**
    * Checks if there is a element with the given selector
    *
    * @param {String} selector jQuery selector
    * @return {Boolean}
    */
   el_exists: function(selector) {
      return $(selector).length > 0;
   },

   /**
    * Creates a CSS compatible string from a JID
    *
    * @param {type} jid Valid Jabber ID
    * @returns {String} css Compatible string
    */
   jidToCid: function(jid) {
      jsxc.warn('jsxc.jidToCid is deprecated!');

      var cid = Strophe.getBareJidFromJid(jid).replace('@', '-').replace(/\./g, '-').toLowerCase();

      return cid;
   },

   /**
    * Create comparable bar jid.
    *
    * @memberOf jsxc
    * @param jid
    * @returns comparable bar jid
    */
   jidToBid: function(jid) {
      return Strophe.unescapeNode(Strophe.getBareJidFromJid(jid).toLowerCase());
   },

   /**
    * Restore roster
    */
   restoreRoster: function() {
      var buddies = jsxc.storage.getUserItem('buddylist');

      if (!buddies || buddies.length === 0) {
         jsxc.debug('No saved buddylist.');

         jsxc.gui.roster.empty();

         return;
      }

      $.each(buddies, function(index, value) {
         jsxc.gui.roster.add(value);
      });

      jsxc.gui.roster.loaded = true;
      $(document).trigger('cloaded.roster.jsxc');
   },

   /**
    * Restore all windows
    */
   restoreWindows: function() {
      var windows = jsxc.storage.getUserItem('windowlist');

      if (windows === null) {
         return;
      }

      $.each(windows, function(index, bid) {
         var win = jsxc.storage.getUserItem('window', bid);

         if (!win) {
            jsxc.debug('Associated window-element is missing: ' + bid);
            return true;
         }

         jsxc.gui.window.init(bid);

         if (!win.minimize) {
            jsxc.gui.window.show(bid);
         } else {
            jsxc.gui.window.hide(bid);
         }

         jsxc.gui.window.setText(bid, win.text);
      });
   },

   /**
    * This method submits the specified login form.
    */
   submitLoginForm: function() {
      var form = $(jsxc.options.loginForm.form).off('submit');

      // Attach original events
      var submits = form.data('submits') || [];
      $.each(submits, function(index, val) {
         form.submit(val);
      });

      if (form.find('#submit').length > 0) {
         form.find('#submit').click();
      } else if (form.get(0) && typeof form.get(0).submit === 'function') {
         form.submit();
      } else if (form.find('[type="submit"]').length > 0) {
         form.find('[type="submit"]').click();
      } else {
         jsxc.warn('Could not submit login form.');
      }
   },

   /**
    * Escapes some characters to HTML character
    */
   escapeHTML: function(text) {
      text = text.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
      return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
   },

   /**
    * Removes all html tags.
    *
    * @memberOf jsxc
    * @param text
    * @returns stripped text
    */
   removeHTML: function(text) {
      return $('<span>').html(text).text();
   },

   /**
    * Executes only one of the given events
    *
    * @param {string} obj.key event name
    * @param {function} obj.value function to execute
    * @returns {string} namespace of all events
    */
   switchEvents: function(obj) {
      var ns = Math.random().toString(36).substr(2, 12);
      var self = this;

      $.each(obj, function(key, val) {
         $(document).one(key + '.' + ns, function() {
            $(document).off('.' + ns);

            val.apply(self, arguments);
         });
      });

      return ns;
   },

   /**
    * Checks if tab is hidden.
    *
    * @returns {boolean} True if tab is hidden
    */
   isHidden: function() {
      var hidden = false;

      if (typeof document.hidden !== 'undefined') {
         hidden = document.hidden;
      } else if (typeof document.webkitHidden !== 'undefined') {
         hidden = document.webkitHidden;
      } else if (typeof document.mozHidden !== 'undefined') {
         hidden = document.mozHidden;
      } else if (typeof document.msHidden !== 'undefined') {
         hidden = document.msHidden;
      }

      // handle multiple tabs
      if (hidden && jsxc.master) {
         jsxc.storage.ink('hidden', 0);
      } else if (!hidden && !jsxc.master) {
         jsxc.storage.ink('hidden');
      }

      return hidden;
   },

   /**
    * Checks if tab has focus.
    *
    * @returns {boolean} True if tabs has focus
    */
   hasFocus: function() {
      var focus = true;

      if (typeof document.hasFocus === 'function') {
         focus = document.hasFocus();
      }

      if (!focus && jsxc.master) {
         jsxc.storage.ink('focus', 0);
      } else if (focus && !jsxc.master) {
         jsxc.storage.ink('focus');
      }

      return focus;
   },

   /**
    * Executes the given function in jsxc namespace.
    *
    * @memberOf jsxc
    * @param {string} fnName Function name
    * @param {array} fnParams Function parameters
    * @returns Function return value
    */
   exec: function(fnName, fnParams) {
      var fnList = fnName.split('.');
      var fn = jsxc[fnList[0]];
      var i;
      for (i = 1; i < fnList.length; i++) {
         fn = fn[fnList[i]];
      }

      if (typeof fn === 'function') {
         return fn.apply(null, fnParams);
      }
   },

   /**
    * Hash string into 32-bit signed integer.
    *
    * @memberOf jsxc
    * @param {string} str input string
    * @returns {integer} 32-bit signed integer
    */
   hashStr: function(str) {
      var hash = 0,
         i;

      if (str.length === 0) {
         return hash;
      }

      for (i = 0; i < str.length; i++) {
         hash = ((hash << 5) - hash) + str.charCodeAt(i);
         hash |= 0; // Convert to 32bit integer
      }

      return hash;
   },

   isExtraSmallDevice: function() {
      return $(window).width() < 500;
   },

   changeState: function(state) {
      jsxc.currentState = state;

      jsxc.debug('State changed to ' + Object.keys(jsxc.CONST.STATE)[state]);

      $(document).trigger('stateChange.jsxc', state);
   },

   changeUIState: function(state) {
      jsxc.currentUIState = state;

      jsxc.debug('UI State changed to ' + Object.keys(jsxc.CONST.UISTATE)[state]);

      $(document).trigger('stateUIChange.jsxc', state);
   }
};