All Downloads are FREE. Search and download functionalities are using the official Maven repository.

toserve.lift.js Maven / Gradle / Ivy

The newest version!
(function(window, undefined) {
  "use strict";

  var hasOwnProperty = Object.prototype.hasOwnProperty;

  window.lift = (function() {
    // "private" vars
    var settings,
        ajaxPath = function() { return settings.liftPath + '/ajax'; },
        ajaxQueue = [],
        ajaxInProcess = null,
        ajaxVersion = 0,
        cometPath = function() { return settings.liftPath + '/comet'; },
        doCycleQueueCnt = 0,
        ajaxShowing = false,
        initialized = false,
        pageId = "",
        uriSuffix,
        sessionId = "",
        toWatch = {},
        knownPromises = {};

    // default settings
    settings = {
      /**
        * Contains the Ajax URI path used by Lift to process Ajax requests.
        */
      liftPath: "/lift",
      ajaxRetryCount: 3,
      ajaxPostTimeout: 5000,

      /**
        * By default lift uses a garbage-collection mechanism of removing unused bound functions from LiftSesssion.
        * Setting this to false will disable this mechanism and there will be no Ajax polling requests attempted.
        */
      enableGc: true,

      /**
        * The polling interval for background Ajax requests to prevent functions of being garbage collected.
        * Default value is set to 75 seconds.
        */
      gcPollingInterval: 75000,

      /**
        * The polling interval for background Ajax requests to keep functions to not be garbage collected.
        * This will be applied if the Ajax request will fail. Default value is set to 15 seconds.
        */
      gcFailureRetryTimeout: 15000,
      logError: function(msg) {
        consoleOrAlert(msg);
      },
      ajaxOnFailure: function() {
        window.alert("The server cannot be contacted at this time");
      },
      ajaxOnStart: function() {
        // noop
      },
      ajaxOnEnd: function() {
        // noop
      },
      ajaxOnSessionLost: function() {
        window.location.reload();
      },
      ajaxPost: function(url, data, dataType, onSuccess, onFailure) {
        consoleOrAlert("ajaxPost function must be defined in settings");
        onFailure();
      },
      ajaxGet: function() {
        consoleOrAlert("ajaxGet function must be defined in settings");
      },
      onEvent: function() {
        consoleOrAlert("onEvent function must be defined in settings");
      },
      onDocumentReady: function() {
        consoleOrAlert("onDocumentReady function must be defined in settings");
      },
      getComets: function() {
        consoleOrAlert("getComets function must be defined in settings");
      },
      cometGetTimeout: 140000,
      cometFailureRetryTimeout: 10000,
      cometOnSessionLost: function(contextPath) {
        window.location.href = contextPath || "/";
      },
      cometServer: null,
      cometOnError: function(e) {
        if (window.console && typeof window.console.error === 'function') {
          window.console.error(e.stack || e);
        }
        throw e;
      }
    };

    // "private" funcs
    function consoleOrAlert(msg) {
      if (window.console && typeof window.console.error === 'function') {
        window.console.error(msg);
      }
      else {
        window.alert(msg);
      }
    }

    ////////////////////////////////////////////////
    ///// Ajax /////////////////////////////////////
    ////////////////////////////////////////////////

    function appendToQueue(data, onSuccess, onFailure, responseType, onUploadProgress) {
      var toSend = {
        retryCnt: 0,
        when: (new Date()).getTime(),
        data: data,
        onSuccess: onSuccess,
        onFailure: onFailure,
        responseType: responseType,
        onUploadProgress: onUploadProgress,
        version: ajaxVersion++
      };

      // Make sure we wrap when we hit JS max int.
      var version = ajaxVersion;
      if ((version - (version + 1) !== -1) || (version - (version - 1) !== 1)) {
        ajaxVersion = 0;
      }

      // for adding a func to call
      if (uriSuffix) {
        data += '&' + uriSuffix;
        toSend.data = data;
        uriSuffix = undefined;
      }

      ajaxQueue.push(toSend);
      ajaxQueueSort();

      if (initialized) {
        doCycleQueueCnt++;
        doAjaxCycle();
      }

      return false; // buttons in forms don't trigger the form
    }

    function ajaxQueueSort() {
      ajaxQueue.sort(function (a, b) { return a.when - b.when; });
    }

    function startAjax() {
      ajaxShowing = true;
      settings.ajaxOnStart();
    }

    function endAjax() {
      ajaxShowing = false;
      settings.ajaxOnEnd();
    }

    function testAndShowAjax() {
      if (ajaxShowing && ajaxQueue.length === 0 && ajaxInProcess === null) {
        endAjax();
      }
      else if (!ajaxShowing && (ajaxQueue.length > 0 || ajaxInProcess !== null)) {
        startAjax();
      }
    }

    /*function traverseAndCall(node, func) {
      if (node.nodeType == 1) {
        func(node);
      }
      var i = 0;
      var cn = node.childNodes;

      for (i = 0; i < cn.length; i++) {
        traverseAndCall(cn.item(i), func);
      }
    }*/

    function calcAjaxUrl(url, version) {
      if (settings.enableGc) {
        var replacement = ajaxPath()+'/'+pageId;
        if (version !== null) {
          replacement += ('-'+version.toString(36)) + (ajaxQueue.length > 35 ? 35 : ajaxQueue.length).toString(36);
        }
        return url.replace(ajaxPath(), replacement);
      }
      else {
        return url;
      }
    }

    function registerGC() {
      var data = "__lift__GC=_";

      settings.ajaxPost(
        calcAjaxUrl(ajaxPath()+"/", null),
        data,
        "script",
        successRegisterGC,
        failRegisterGC
      );
    }

    function successRegisterGC() {
      setTimeout(registerGC, settings.gcPollingInterval);
    }

    function failRegisterGC() {
      setTimeout(registerGC, settings.gcFailureRetryTimeout);
    }

    function doCycleIn200() {
      doCycleQueueCnt++;
      setTimeout(doAjaxCycle, 200);
    }

    function doAjaxCycle() {
      if (doCycleQueueCnt > 0) {
        doCycleQueueCnt--;
      }

      var queue = ajaxQueue;
      if (queue.length > 0) {
        var now = (new Date()).getTime();
        if (ajaxInProcess === null && queue[0].when <= now) {
          var aboutToSend = queue.shift();

          ajaxInProcess = aboutToSend;

          var successFunc = function(data) {
            ajaxInProcess = null;
            if (aboutToSend.onSuccess) {
              aboutToSend.onSuccess(data);
            }
            doCycleQueueCnt++;
            doAjaxCycle();
          };

          var failureFunc = function() {
            ajaxInProcess = null;
            var cnt = aboutToSend.retryCnt;

            if (arguments.length === 3 && arguments[1] === 'parsererror') {
              settings.logError('The server call succeeded, but the returned Javascript contains an error: '+arguments[2]);
            }

            if (cnt < settings.ajaxRetryCount) {
              aboutToSend.retryCnt = cnt + 1;
              var now = (new Date()).getTime();
              aboutToSend.when = now + (1000 * Math.pow(2, cnt));
              queue.push(aboutToSend);
              ajaxQueueSort();
            }
            else {
              if (aboutToSend.onFailure) {
                aboutToSend.onFailure();
              }
              else {
                settings.ajaxOnFailure();
              }
            }
            doCycleQueueCnt++;
            doAjaxCycle();
          };

          if (aboutToSend.responseType !== undefined &&
              aboutToSend.responseType !== null &&
              aboutToSend.responseType.toLowerCase() === "json")
          {
            settings.ajaxPost(
              calcAjaxUrl(ajaxPath()+"/", null),
              aboutToSend.data,
              "json",
              successFunc,
              failureFunc,
              aboutToSend.onUploadProgress
            );
          }
          else {
            settings.ajaxPost(
              calcAjaxUrl(ajaxPath()+"/", aboutToSend.version),
              aboutToSend.data,
              "script",
              successFunc,
              failureFunc,
              aboutToSend.onUploadProgress
            );
          }
        }
      }

      testAndShowAjax();
      if (doCycleQueueCnt <= 0) {
        doCycleIn200();
      }
    }

    ////////////////////////////////////////////////
    ///// Comet ////////////////////////////////////
    ////////////////////////////////////////////////

    var currentCometRequest = null,
        // Used to ensure that we can only fire one comet request at a time.
        cometRequestCount = 0;

    // http://stackoverflow.com/questions/4994201/is-object-empty
    function is_empty(obj) {
      // null and undefined are empty
      /* jshint eqnull:true */
      if (obj == null) {
        return true;
      }
      /* jshint eqnull:false */

      // Assume if it has a length property with a non-zero value
      // that that property is correct.
      if (obj.length && obj.length > 0) {
        return false;
      }
      if (obj.length === 0) {
        return true;
      }

      for (var key in obj) {
        if (hasOwnProperty.call(obj, key)) {
          return false;
        }
      }
      // Doesn't handle toString and toValue enumeration bugs in IE < 9
      return true;
    }

    function cometFailureFunc() {
      var requestCount = cometRequestCount;
      setTimeout(function() { cometEntry(requestCount); }, settings.cometFailureRetryTimeout);
    }

    function cometSuccessFunc() {
      var requestCount = cometRequestCount;
      setTimeout(function() { cometEntry(requestCount); }, 100);
    }

    function calcCometPath() {
      var fullPath = cometPath()+ "/" + Math.floor(Math.random() * 100000000000) + "/" + sessionId + "/" + pageId;
      if (settings.cometServer) {
        return settings.cometServer + fullPath;
      } else {
        return fullPath;
      }
    }

    // Forcibly restart the comet cycle; use this, for example, when a
    // new comet has been received.
    function restartComet() {
      if (currentCometRequest) {
        currentCometRequest.abort();
      }

      cometSuccessFunc();
    }

    function cometEntry(requestedCount) {
      var isEmpty = is_empty(toWatch);

      if (!isEmpty && requestedCount === cometRequestCount) {
        uriSuffix = undefined;
        cometRequestCount++;
        currentCometRequest =
          settings.ajaxGet(
            calcCometPath(),
            toWatch,
            cometSuccessFunc,
            cometFailureFunc
          );
      }
    }

    function unlistWatch(watchId) {
      var ret = [];
      for (var item in toWatch) {
        if (item !== watchId) {
          ret.push(item);
        }
      }
      toWatch = ret;
    }

    // Called to register comets in bulk. `cometInfo` should be
    // an object of comet ids associated with comet versions.
    //
    // If startComet is passed and true, restarts the comet request
    // cycle.
    function registerComets(cometInfo, startComet) {
      for (var cometGuid in cometInfo) {
        toWatch[cometGuid] = cometInfo[cometGuid];
      }

      if (startComet) {
        restartComet();
      }
    }

    function rehydrateComets() {
      function onSuccess(html) {
        var iframe = document.createElement("iframe");
        var doc = document.createElement("html");
        iframe.appendChild(doc);
        doc.innerHTML = html;

        toWatch = {};

        var newComets = settings.getComets(doc);
        var oldComets = settings.getComets(document);
        for (var i = 0, n = newComets.length; i < n; i++) {
          var version = newComets[i].getAttribute("data-lift-comet-version");
          var guid = newComets[i].children[0].getAttribute("id");
          toWatch[guid] = version;
          oldComets[i].outerHTML = newComets[i].outerHTML;
        }

        restartComet();
      }
      function onFailure(err) {
        // Try again??
        settings.logError(err);
      }

      settings.ajaxGet(location.toString(), {}, onSuccess, onFailure, "html");
    }


    ////////////////////////////////////////////////
    ///// Promises /////////////////////////////////
    ////////////////////////////////////////////////
    function randStr() {
      return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
    }

    function makeGuid() {
      return randStr() + randStr() + '-' + randStr() + '-' + randStr() + '-' +
             randStr() + '-' + randStr() + randStr() + randStr();
    }

    function removePromise(g) {
      knownPromises[g] = undefined;
    }

    function Promise() {
      // "private" vars
      var self = this,
          _values = [],
          _events = [],
          _failMsg = "",
          _valueFuncs = [],
          _doneFuncs = [],
          _failureFuncs = [],
          _eventFuncs = [],
          _done = false,
          _failed = false;

      // "private" funcs
      function successMsg(value) {
        if (_done || _failed) { return; }
        _values.push(value);
        for (var f in _valueFuncs) {
          _valueFuncs[f](value);
        }
      }

      function failMsg(msg) {
        if (_done || _failed) { return; }
        removePromise(self.guid);
        _failed = true;
        _failMsg = msg;

        for (var f in _failureFuncs) {
          _failureFuncs[f](msg);
        }
      }

      function doneMsg() {
          if (_done || _failed) { return; }
        removePromise(self.guid);
        _done = true;

        for (var f in _doneFuncs) {
          _doneFuncs[f]();
        }
      }

      // public funcs
      self.guid = makeGuid();

      self.processMsg = function(evt) {
        if (_done || _failed) { return; }
        _events.push(evt);
        for (var v in _eventFuncs) {
          try { _eventFuncs[v](evt); }
          catch (e) {
            settings.logError(e);
          }
        }

        /* jshint eqnull:true */
        if (evt.done != null) {
          doneMsg();
        }
        else if (evt.success != null) {
          successMsg(evt.success);
        }
        else if (evt.failure != null) {
          failMsg(evt.failure);
        }
        /* jshint eqnull:false */
      };

      self.then = function(f) {
        _valueFuncs.push(f);

        for (var v in _values) {
          try { f(_values[v]); }
          catch (e) {
            settings.logError(e);
          }
        }

        return self;
      };

      self.fail = function(f) {
        _failureFuncs.push(f);
        if (_failed) {
          try { f(_failMsg); }
          catch (e) {
            settings.logError(e);
          }
        }

        return self;
      };

      self['catch'] = self.fail;

      self.done = function(f) {
        _doneFuncs.push(f);
        if (_done) {
          try { f(); }
          catch (e) {
            settings.logError(e);
          }
        }

        return this;
      };

      self.onEvent = function(f) {
        _eventFuncs.push(f);
        for (var v in _events) {
          try { f(_events[v]); }
          catch (e) {
            settings.logError(e);
          }
        }

        return this;
      };

      self.map = function(f) {
        var ret = new Promise();

        self.done(function() {
          ret.doneMsg();
        });

        self.fail(function (m) {
          ret.failMsg(m);
        });

        self.then(function (v) {
          ret.successMsg(f(v));
        });

        return ret;
      };
    }

    ////////////////////////////////////////////////
    ///// Public Object /////////////////////////////////
    ////////////////////////////////////////////////
    return {
      init: function(options) {
        // override default settings
        this.extend(settings, options);

        var lift = this;
        settings.onDocumentReady(function() {
          var attributes = document.body.attributes,
              cometGuid, cometVersion,
              comets = {};
          for (var i = 0; i < attributes.length; ++i) {
            if (attributes[i].name === 'data-lift-gc') {
              pageId = attributes[i].value;
              if (settings.enableGc) {
                lift.startGc();
              }
            } else if (attributes[i].name.match(/^data-lift-comet-/)) {
              cometGuid = attributes[i].name.substring('data-lift-comet-'.length).toUpperCase();
              cometVersion = parseInt(attributes[i].value, 10);

              comets[cometGuid] = cometVersion;
            } else if (attributes[i].name === 'data-lift-session-id') {
              sessionId = attributes[i].value;
            }
          }

          if (typeof cometGuid !== 'undefined') {
            registerComets(comets, true);
          }

          initialized = true;

          // start the cycle
          doCycleIn200();
        });
      },
      defaultLogError: function(msg) { consoleOrAlert(msg); },
      logError: function() { settings.logError.apply(this, arguments); },
      onEvent: function() { settings.onEvent.apply(this, arguments); },
      ajax: appendToQueue,
      startGc: successRegisterGC,
      ajaxOnSessionLost: function() {
        settings.ajaxOnSessionLost();
      },
      calcAjaxUrl: calcAjaxUrl,
      registerComets: registerComets,
      cometOnSessionLost: function(contextPath) {
        settings.cometOnSessionLost(contextPath);
      },
      rehydrateComets: rehydrateComets,
      cometOnError: function(e) {
        settings.cometOnError(e);
      },
      unlistWatch: unlistWatch,
      setToWatch: function(tw) {
        toWatch = tw;
      },
      setPageId: function(pgId) {
        pageId = pgId;
      },
      getPageId: function() {
        return pageId;
      },
      setUriSuffix: function(suffix) {
        uriSuffix = suffix;
      },
      updWatch: function(id, when) {
        if (toWatch[id] !== undefined) {
          toWatch[id] = when;
        }
      },
      extend: function(obj1, obj2) {
        for (var item in obj2) {
          if (hasOwnProperty.call(obj2, item)) {
            obj1[item] = obj2[item];
          }
        }
      },
      createPromise: function() {
        var promise = new Promise();
        knownPromises[promise.guid] = promise;
        return promise;
      },
      sendEvent: function(g, evt) {
        var p = knownPromises[g];
        if (p) {
          p.processMsg(evt);
        }
      }
    };
  })();

  window.liftJQuery = {
    onEvent: function(elementOrId, eventName, fn) {
      if (typeof elementOrId === 'string') {
        elementOrId = document.getElementById(elementOrId);
      }

      jQuery(elementOrId).on(eventName, fn);
    },
    onDocumentReady: function(fn) {
      jQuery(document).ready(fn);
    },
    ajaxPost: function(url, data, dataType, onSuccess, onFailure) {
      var processData = true,
          contentType = 'application/x-www-form-urlencoded; charset=UTF-8';

      if (typeof data === "object") { // FormData
        processData = false;  // tell jQuery not to process the data
        contentType = false; // tell jQuery not to set contentType
      }

      jQuery.ajax({
        url: url,
        data: data,
        type: "POST",
        dataType: dataType,
        timeout: this.ajaxPostTimeout,
        cache: false,
        success: onSuccess,
        error: onFailure,
        processData: processData,
        contentType: contentType
      });
    },
    ajaxGet: function(url, data, onSuccess, onFailure, dataType) {
      if (!dataType) {
        dataType = "script";
      }

      return jQuery.ajax({
        url: url,
        data: data,
        type: "GET",
        dataType: dataType,
        timeout: this.cometGetTimeout,
        cache: false,
        success: onSuccess,
        error: function(_, status) {
          if (status !== 'abort') {
            return onFailure.apply(this, arguments);
          }
        }
      });
    },
    getComets: function(doc) {
      return jQuery(doc).find("[data-lift-comet-version]").toArray();
    }
  };

  window.liftVanilla = {
    // This and onDocumentReady adapted from https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js,
    // as also used (with modifications) in jQuery.
    onEvent: function(elementOrId, eventName, fn) {
      var win = window,
          doc = win.document,
          add = doc.addEventListener ? 'addEventListener' : 'attachEvent',
          pre = doc.addEventListener ? '' : 'on';

      var element = elementOrId;
      if (typeof elementOrId === 'string') {
        element = document.getElementById(elementOrId);
      }

      // This is a Lift addition to allow return false to properly do
      // cross-browser preventDefault/stopPropagation/etc work.
      function normalizeEventReturn(event) {
        var result = fn(event);
        if (result === false) {
          if (typeof event.preventDefault === "function") {
            event.preventDefault();
            event.stopPropagation();
          }
        }

        return result;
      }

      element[add](pre + eventName, normalizeEventReturn, false);
    },
    onDocumentReady: function(fn) {
      var settings = this, done = false, top = true,
      win = window, doc = win.document, root = doc.documentElement,
      pre = doc.addEventListener ? '' : 'on',
      rem = doc.addEventListener ? 'removeEventListener' : 'detachEvent',

      init = function(e) {
        if (e.type === 'readystatechange' && doc.readyState !== 'complete') { return; }
        (e.type === 'load' ? win : doc)[rem](pre + e.type, init, false);
        if (!done && (done = true)) { fn.call(win, e.type || e); }
      },

      poll = function() {
        try { root.doScroll('left'); } catch(e) { setTimeout(poll, 50); return; }
        init('poll');
      };

      if (doc.readyState === 'complete') {
        fn.call(win, 'lazy');
      } else {
        if (doc.createEventObject && root.doScroll) {
          try { top = !win.frameElement; } catch(e) { }
          if (top) { poll(); }
        }
        settings.onEvent(doc, 'DOMContentLoaded', init);
        settings.onEvent(doc, 'readystatechange', init);
        settings.onEvent(win, 'load', init);
      }
    },
    ajaxPost: function(url, data, dataType, onSuccess, onFailure, onUploadProgress) {
      var settings = this;

      var xhr = new XMLHttpRequest();

      if (onUploadProgress) {
        xhr.upload.addEventListener("progress", onUploadProgress, false);
      }

      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) { // Done
          if (xhr.status === 200) {
            if (dataType === "script") {
              try {
                eval(xhr.responseText); // jshint ignore:line
              }
              catch (e) {
                settings.logError('The server call succeeded, but the returned Javascript contains an error: '+e);
              }
              finally {
                onSuccess();
              }
            }
            else if (dataType === "json") {
              var obj = {};
              try {
                obj = JSON.parse(xhr.responseText);
              }
              catch(e) {
                settings.logError('The server call succeeded, but the returned JSON contains an error: '+e);
              }
              finally {
                onSuccess(obj);
              }
            }
            else if (dataType === "html") {
              onSuccess(xhr.responseText);
            }
            else {
              settings.logError("Unknown data type: "+dataType);
            }
          }
          else {
            onFailure();
          }
        }
      };

      xhr.open("POST", url, true);
      xhr.timeout = settings.ajaxPostTimeout;
      xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

      // set content-type header if the form has been serialized into a string
      if (typeof data === "string") {
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
      }

      // These just mimic what jQuery produces
      if (dataType === "script") {
        xhr.setRequestHeader("Accept", "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01");
      }
      else if (dataType === "json") {
        xhr.setRequestHeader("Accept", "application/json, text/javascript, */*; q=0.01");
      }
      xhr.send(data);
    },
    ajaxGet: function(url, data, onSuccess, onFailure) {
      var settings = this;

      // create query string
      var qs = "";
      for (var key in data) {
        if (qs !== "") {
          qs += "&";
        }
        qs += key + "=" + data[key];
      }

      var xhr = new XMLHttpRequest();

      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) { // Done
          if (xhr.status === 200) {
            try {
              eval(xhr.responseText); // jshint ignore:line
            }
            catch (e) {
              settings.logError('The server call succeeded, but the returned Javascript contains an error: '+e);
            }
            finally {
              onSuccess();
            }
          // done + 0 status = aborted, or at least it's the most
          // straightforward proxy that we have; ready state = 0 is
          // supposed to be aborted, but that's all kinds of not
          // working, unfortunately. jQuery's approach is better,
          // but they have a lot more state tracking to achieve it.
          } else if (xhr.status !== 0) {
            onFailure();
          }
        }
      };

      if (qs !== "") {
        url = url+"?"+qs;
      }

      xhr.open("GET", url, true);
      xhr.timeout = settings.cometGetTimeout;
      xhr.setRequestHeader("Accept", "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01");
      xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
      xhr.send();

      return xhr;
    },
    getComets: function(doc) {
      if(doc.querySelectorAll && typeof doc.querySelectorAll === 'function') {
        var nodes = doc.querySelectorAll("[data-lift-comet-version]");
        return Array.prototype.slice.call(nodes, 0);
      }
      else {
        var divs = doc.getElementsByTagName("div");
        var comets = [];
        for (var i = 0, n = divs.length; i < n; i++) {
          if (divs[i].getAttribute("data-lift-comet-version") !== null) {
            comets.push(divs[i]);
          }
        }
        return comets;
      }
    }
  };

  window.liftUtils = {
    lift_blurIfReturn: function(e) {
      var code;
      if (!e) {
        e = window.event;
      }
      if (e.keyCode) {
        code = e.keyCode;
      }
      else if (e.which) {
        code = e.which;
      }

      var targ;

      if (e.target) {
        targ = e.target;
      }
      else if (e.srcElement) {
        targ = e.srcElement;
      }
      if (targ.nodeType === 3) { // defeat Safari bug
        targ = targ.parentNode;
      }
      if (code === 13) {
        targ.blur();
        return false;
      }
      else {
        return true;
      }
    }
  };

})(this);




© 2015 - 2024 Weber Informatics LLC | Privacy Policy