privly/privly-applications

View on GitHub
shared/javascripts/viewAdapters/new.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * @fileOverview This is a general adapter for interfacing with the Privly-web
 * server: github.com/privly/privly-web
 *
 * Its purpose is to allow all privly-web derived applications to interface
 * with privly-web through a consistent adapter, thereby speeding updates
 * and easing the generation of a large number of applications.
 *
 * It manages the general flow of an application that interfaces with the 
 * privly-web server but it is intended to be extended as necessary by
 * individual privly-applications. For example, see new.js in the PlainPost
 * Privly Application.
 *
 * ## Expected Elements ##
 * This adapter expects the DOM to have a set of elements
 *
 * '#loadingDiv': This will be shown whenever an AJAX request is pending
 * '#messages': This shows UI messages to the user.
 * '#login_message': This shows a login error message to the user.
 * '#save': This is the submit button for the form.
 * '#form': This is the form the user enters content into.
 * '.privlyUrl': The text with the Privly-type URLs.
 * '#content")[0]': The content area that accepts user text.
 *
 * ## Extending this script ##
 * See Message/js/messageApp.js and Message/js/new.js for examples.
 *
 **/
/*global privlyNetworkService, window */
// If Privly namespace is not initialized, initialize it
var Privly;
if (Privly === undefined) {
  Privly = {};
}
if (Privly.app === undefined) {
  Privly.app = {};
}
if (Privly.app.viewAdapter === undefined) {
  Privly.app.viewAdapter = {};
}

(function () {
  // If this file is already loaded, don't do it again
  if (Privly.app.viewAdapter.New !== undefined) {
    return;
  }

  /**
   * These applications can be placed into the following states:
   * 1. Pending Login Check: The app is currently requesting the CSRF
   *    token from the remote server.
   * 2. Failure to login: The user is not currently authenticated with the
   *    remote server. In this state the user is prompted to login.
   * 3. Pending post: The user can make the post at this point.
   * 4. postSubmit: The user submitted the form so the content is being
   *    sent to the remote server. Once it is returned, the URL will
   *    be messaged to the extension (if present) by calling the
   *    "postCompleted" function.
   * 5. Error creating post: The remote server would not accept the user's
   *    content. The app should display an error message.
   * 6. Completed post: The remote server has returned a URL. This app should
   *    display it and fire the URL event.
   */
  var NewAdapter = function (application) {
    /**
     * The Privly application instance.
     * 
     * @type {Object}
     */
    this.application = application;

    /**
     * Whether the adapter is started
     * 
     * @type {Boolean}
     */
    this.started = false;
  };

  // Inhreit EventEmitter
  Privly.EventEmitter.inherit(NewAdapter);

  Privly.app.viewAdapter.New = NewAdapter;

  /**
   * Method factory. Add overridable function to NewAdapter.
   * It will create a function which behavior can be overridden by
   * listening `before{name}` event and `after{name}` event.
   * 
   * @param {String} name The name of the function.
   * @param {Function} func The body of the function if `before` event
   * doesn't return true=preventDefault.
   */
  var addOverridableFunction = function (name, func) {
    var eventName = name.substr(0, 1).toUpperCase() + name.substr(1);
    NewAdapter.prototype[name] = function () {
      var self = this;
      var argv = [].slice.call(arguments);
      return self
        .emitAsync.apply(self, ['before' + eventName].concat(argv))
        .then(function (preventDefault) {
          if (preventDefault === true) {
            return;
          }
          return func.apply(self, argv);
        })
        .then(function () {
          return self.emitAsync.apply(self, ['after' + eventName].concat(argv));
        });
    };
  };

  /**
   * Start the creation process adapter.
   * It will create the default navigation bar, assign ajax request listeners and
   * finally call `pendingLogin()` to start the login checking process.
   *
   * @return {Promise}
   */
  NewAdapter.prototype._start = function () {
    var self = this;
    if (self.started) {
      return;
    }
    self.started = true;

    // Set the nav bar to the proper domain
    privlyNetworkService.initializeNavigation();

    // Add listeners to show loading animation while making ajax requests
    $(document).ajaxStart(function () {
      $('#loadingDiv').show();
    });
    $(document).ajaxStop(function () {
      $('#loadingDiv').hide();
    });

    self.pendingLogin();
  };
  addOverridableFunction('start', NewAdapter.prototype._start);

  /**
   * When server connection checking succeeded.
   * It will add event listeners to the submit button and
   * make textarea able to auto resize. Finally it will call
   * `pendingPost()` to prepare the posting form.
   *
   * @return {Promise}
   */
  NewAdapter.prototype._connectionSucceeded = function () {
    var self = this;

    // Monitor the submit button
    $(document).on('click', '#save', self.save.bind(self));

    // Make all text areas auto resize to show all their contents
    $('textarea').autosize();

    self.pendingPost();
  };
  addOverridableFunction('connectionSucceeded', NewAdapter.prototype._connectionSucceeded);

  /**
   * When server connection checking failed.
   * It will call `loginFailure()` to show error messages.
   *
   * @return {Promise}
   */
  NewAdapter.prototype._connectionFailed = function () {
    var self = this;
    self.loginFailure();
  };
  addOverridableFunction('connectionFailed', NewAdapter.prototype._connectionFailed);

  /**
   * Get processed request content from the Privly application.
   * 
   * @param  {String} content
   * @return {Promise<Object>}
   *           {String} content
   *           {String} structured_content
   *           {Boolean} isPublic
   */
  NewAdapter.prototype.getRequestContent = function (content) {
    var promise;
    if (typeof this.application.getRequestContent === 'function') {
      promise = this.application.getRequestContent(content);
    } else {
      promise = Promise.resolve({});
    }
    return promise.then(function (reqContent) {
      if (reqContent.content === undefined) {
        reqContent.content = content;
      }
      if (reqContent.structured_content === undefined) {
        reqContent.structured_content = content;
      }
      if (reqContent.isPublic === undefined) {
        reqContent.isPublic = true;
      }
      return reqContent;
    });
  };

  /**
   * Get processed Privly link from the Privly application.
   * Privly application may manipulate the url to add additional
   * information.
   * 
   * @param  {String} link
   * @return {Promise<String>} The processed link
   */
  NewAdapter.prototype.postprocessLink = function (link) {
    var promise;
    if (typeof this.application.postprocessLink === 'function') {
      promise = this.application.postprocessLink(link);
    } else {
      promise = Promise.resolve(link);
    }
    return promise;
  };

  /**
   * Submit the content to the remote server.
   * 
   * @return {Promise}
   */
  NewAdapter.prototype._save = function () {
    var self = this;
    return self
      .getRequestContent($("#content")[0].value)
      .then(function (json) {
        self.postSubmit(
          json.structured_content,
          self.application.name,
          $("#seconds_until_burn").val(),
          json.content
        );
      });
  };
  addOverridableFunction('save', NewAdapter.prototype._save);

  /**
   * Start login process. It will call `connectionSucceeded()` when
   * the checking is passed and call `connectionFailed()` in other
   * cases.
   *
   * @return {Promise}
   */
  NewAdapter.prototype._pendingLogin = function () {
    var self = this;

    Privly.storage.set("Login:redirect_to_app", window.location.href);
    privlyNetworkService.initPrivlyService(
      privlyNetworkService.contentServerDomain(),
      self.connectionSucceeded.bind(self),
      self.connectionFailed.bind(self),
      self.connectionFailed.bind(self)
    );
  };
  addOverridableFunction('pendingLogin', NewAdapter.prototype._pendingLogin);

  /**
   * Prompt the user to sign into their server. This assumes the remote
   * server's sign in endpoint is at "/users/sign_in".
   *
   * @return {Promise}
   */
  NewAdapter.prototype._loginFailure = function () {
    privlyNetworkService.showLoggedOutNav();
    $("#messages").hide();
    $("#login_message").show();
  };
  addOverridableFunction('loginFailure', NewAdapter.prototype._loginFailure);

  /**
   * Tell the user they can create their post by updating the UI.
   *
   * @return {Promise}
   */
  NewAdapter.prototype._pendingPost = function () {
    privlyNetworkService.showLoggedInNav();
    $("#save").prop('disabled', false);
    $("#messages").toggle();
    $("#form").toggle();
  };
  addOverridableFunction('pendingPost', NewAdapter.prototype._pendingPost);

  /**
   * Submit the posting form and await the return of the post.
   *
   * @param {json} structured_content The JSON the application
   * needs to store.
   * @param {string} privly_application The name of the Privly application
   * generating the content.
   * @param {int} seconds_until_burn The number of seconds until this
   * content should be destroyed by the server.
   * @param {string} content A markdown string. This will likely
   * be deprecated in the future.
   * @return {Promise}
   */
  NewAdapter.prototype._postSubmit = function (structured_content, privly_application, seconds_until_burn, content) {
    var self = this;

    $("#save").prop('disabled', true);

    var contentToPost = {
      post: {
        "content": content,
        "structured_content": structured_content,
        "privly_application": privly_application,
        "seconds_until_burn": seconds_until_burn,
        "public": true
      },
      "format": "json"
    };

    // Send the post
    privlyNetworkService.sameOriginPostRequest(
      privlyNetworkService.contentServerDomain() + "/posts",
      function (response) {
        var url = response.jqXHR.getResponseHeader("X-Privly-Url");
        self.postprocessLink(url).then(function (url) {
          self.postCompleted(response, url);
        });
      },
      contentToPost
    );
  };
  addOverridableFunction('postSubmit', NewAdapter.prototype._postSubmit);

  /**
   * Tell the user that there was a problem.
   *
   * @param {jqXHR} response The AJAX response from the server.
   * @return {Promise}
   */
  NewAdapter.prototype._createError = function (response) {
    $("#save").prop('disabled', false);
    $("#messages").text("There was an error creating your post. Status: " + response.jqXHR.status);
    $("#messages").show();
  };
  addOverridableFunction('createError', NewAdapter.prototype._createError);

  /**
   * Send the URL to the extension or mobile device if it exists and display
   * it to the end user.
   *
   * @param {jqXHR} response The AJAX response from the server.
   * @param {string} url The injectable URL for the Privly Application.
   * @return {Promise}
   */
  NewAdapter.prototype._postCompleted = function (response, url) {
    $("#save").prop('disabled', false);

    if (response.jqXHR.status === 201 && url !== undefined && url !== "") {
      Privly.message.messageExtension({privlyUrl: url});

      $("#copy_message").show();

      $('#local_address').attr("href", url);
      if (privlyNetworkService.platformName() !== "HOSTED") {
        var localCodeURL = "show.html?privlyOriginalURL=" + encodeURIComponent(url);
        $('#local_address').attr("href", localCodeURL);
      }

      $(".privlyUrl").text(url);
      $(".privlyUrl").css("cursor", "pointer");
      $(".privlyUrl").click(function () {
        var range, selection;

        if (window.getSelection && document.createRange) {
          selection = window.getSelection();
          range = document.createRange();
          range.selectNodeContents($(this)[0]);
          selection.removeAllRanges();
          selection.addRange(range);
        } else if (document.selection && document.body.createTextRange) {
          range = document.body.createTextRange();
          range.moveToElementText($(this)[0]);
          range.select();
        }
        $(".open-app-button").show();
      });
    }
  };
  addOverridableFunction('postCompleted', NewAdapter.prototype._postCompleted);

}());