Source: js/views/EditorView.js

define(['underscore',
        'jquery',
        'backbone',
        "views/SignInView",
        "text!templates/editorSubmitMessage.html"],
function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){

  /**
  * @class EditorView
  * @classdesc A basic shell of a view, primarily meant to be extended for views that allow editing capabilities.
  * @name EditorView
  * @extends Backbone.View
  * @constructs
  */
  var EditorView = Backbone.View.extend(
    /** @lends EditorView.prototype */{


    /**
    * References to templates for this view. HTML files are converted to Underscore.js templates
    */
    editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),

    /**
    * The element this view is contained in. A jQuery selector or the element itself.
    * @type {string|DOMElement}
    */
    el: "#Content",

    /**
    * The text to use in the editor submit button
    * @type {string}
    */
    submitButtonText: "Save",

    /**
    * The text to use in the editor submit button
    * @type {string}
    */
    accessPolicyModalID: "editor-access-policy-modal",

    /**
    * The selector for the HTML element that will contain a button/link/control for
    * opening the AccessPolicyView modal window. If this element doesn't exist on the page,
    * then the AccessPolicyView will be inserted into the `accessPolicyViewContainer` directly, rather than a modal window.
    * @type {string}
    */
    accessPolicyControlContainer: ".access-policy-control-container",

    /**
    * The selector for the HTML element that will contain the AccessPolicyView.
    * If this element doesn't exist on the page, then the AccessPolicyView will not be inserted into the page.
    * If a `accessPolicyControlContainer` element is on the page, then this element will
    * contain the modal window element.
    * @type {string}
    */
    accessPolicyViewContainer: ".access-policy-view-container",
    /**
    * The events this view will listen to and the associated function to call
    * @type {Object}
    */
    events: {
      "click #save-editor" : "save",
      "click .access-policy-control" : "showAccessPolicyModal",
      "keypress input" : "showControls",
      "keypress textarea" : "showControls",
      "keypress [contenteditable]" : "showControls"
    },

    /**
    * Renders this view
    */
    render: function(){
      //Style the body as an Editor
      $("body").addClass("Editor rendering");

      this.delegateEvents();
    },

    /**
     * Set listeners on the view's model.
     * This function centralizes all the listeners so that when/if the view's
     * model is replaced, the listeners can be reset.
     */
    setListeners: function() {

      //Stop listening first
      this.stopListening(this.model, "errorSaving", this.saveError);
      this.stopListening(this.model, "successSaving", this.saveSuccess);
      this.stopListening(this.model, "invalid", this.showValidation);

      //Set listeners
      this.listenTo(this.model, "errorSaving", this.saveError);
      this.listenTo(this.model, "successSaving", this.saveSuccess);
      this.listenTo(this.model, "invalid", this.showValidation);

      //Set a beforeunload event only if there isn't one already
      if( !this.beforeunloadCallback ){
        var view = this;
        //When the Window is about to be closed, show a confirmation message
        this.beforeunloadCallback = function(e){
          if( !view.canClose() ){
            //Browsers don't support custom confirmation messages anymore,
            // so preventDefault() needs to be called or the return value has to be set
            e.preventDefault();
            e.returnValue = "";
          }
          return;
        }
        window.addEventListener("beforeunload", this.beforeunloadCallback);
      }
    },

    /**
    * Show Sign In buttons
    */
    showSignIn: function(){
      var container = $(document.createElement("div")).addClass("container center");
      this.$el.html(container);
      var signInButtons = new SignInView().render().el;
      $(container).append('<h1>Sign in to submit data</h1>', signInButtons);
    },

    /**
    * Saves the model
    */
    save: function(){
      this.showSaving();
      this.model.save();
    },

    /**
     * Cancel all edits in the editor by simply re-rendering the view
     */
    cancel: function(){
      this.render();
    },

    /**
    * Trigger a save error with a message that the save was cancelled
    */
    handleSaveCancel: function(){
      if(this.model.get("uploadStatus") == "e"){
        this.saveError("Your submission was cancelled due to an error.");
      }
    },

    /**
    * Adds top-level control elements to this editor.
    */
    renderEditorControls: function(){
      //If the AccessPolicy editor is enabled, add a button for opening it
      if( MetacatUI.appModel.get("allowAccessPolicyChanges")){
        this.renderAccessPolicyControl();
      }
    },

    /**
    * Adds a Share button for editing the access policy
    */
    renderAccessPolicyControl: function(){
      //If the AccessPolicy editor is enabled, add a button for opening it
      if( MetacatUI.appModel.get("allowAccessPolicyChanges") ){

        //Render the AccessPolicy control, if the container element is on the page
        if( this.$(this.accessPolicyControlContainer).length ){
          //If it isn't, then add it to the page.
          //Create an anchor tag with an icon and the text "Share" and add it to the editor controls container
          this.$(this.accessPolicyControlContainer).prepend( $(document.createElement("a"))
                                                    .attr("href", "#")
                                                    .addClass("access-policy-control btn")
                                                    .append(
                                                      $(document.createElement("i")).addClass("icon-group icon icon-on-left"),
                                                      "Share") );
        }
        else{
          this.renderAccessPolicy();
        }

        //Check that this user is authorized to change permissions on this object
        var view = this,
            checkAuthorityOptions = {
              onSuccess: function(){
                return;
              },
              onError: function(){
                //Disable the button for the AccessPolicyView if the user is not authorized
                view.$(".access-policy-control").attr("disabled", "disabled")
                                                .attr("title", "You do not have access to change the " + MetacatUI.appModel.get("accessPolicyName"))
                                                .addClass("disabled");
              }
        }

        //Check the user's authority to change permissions on this object
        this.model.checkAuthority("changePermission", checkAuthorityOptions);
      }
    },

    /**
    * Shows the AccessPolicyView for the object being edited.
    * @param {Event} e - The event that triggered this function as a callback
    */
    showAccessPolicyModal: function(e){

      try{

        //If the AccessPolicy editor is disabled in this app, then exit now
        if( !MetacatUI.appModel.get("allowAccessPolicyChanges") || this.$(".access-policy-control").attr("disabled") == "disabled" ){
          return;
        }

       //If the AccessPolicyView hasn't been rendered yet, then render it now
       if( !this.$(".access-policy-view").length ){
         this.renderAccessPolicy();

         this.on("accessPolicyViewRendered", function(){
           //Add modal classes to the access policy view
           this.$(".access-policy-view").addClass("access-policy-view-modal modal")
                                        .modal()
                                        .modal("show");
         });
       }
       else{
         //Open the modal window
         this.$("access-policy-view-modal").modal("show");
       }

      }
      catch(e){
        console.error("Error trying to show the AccessPolicyView: ", e);
      }
    },

    /**
    * Renders the AccessPolicyView
    * @param {Event} e - The event that triggered this function as a callback
    */
    renderAccessPolicy: function(){
      try{

        //If the AccessPolicy editor is disabled in this app, then exit now
        if( !MetacatUI.appModel.get("allowAccessPolicyChanges")){
          return;
        }

        var thisView = this;
        require(['views/AccessPolicyView'], function(AccessPolicyView){

            //If not, create a new AccessPolicyView using the AccessPolicy collection
            var accessPolicyView = new AccessPolicyView();
            accessPolicyView.collection = thisView.model.get("accessPolicy");

            //Add the view to the page
            thisView.$(thisView.accessPolicyViewContainer).html(accessPolicyView.el);

            //Render the AccessPolicyView
            accessPolicyView.render();

            thisView.trigger("accessPolicyViewRendered");
        });
      }
      catch(e){
        console.error("Error trying to render the AccessPolicyView: ", e);
      }
    },

    /**
    * Show the editor footer controls (Save bar)
    */
    showControls: function(){
      this.$(".editor-controls").removeClass("hidden").slideDown();
    },

    /**
    * Hide the editor footer controls (Save bar)
    */
    hideControls: function(){
        this.hideSaving();

      this.$(".editor-controls").slideUp();
    },

    /**
    * Change the styling of this view to show that the object is in the process of saving
    */
    showSaving: function(){

      //Change the style of the save button
      this.$("#save-editor")
        .html('<i class="icon icon-spinner icon-spin"></i> Submitting ...')
        .addClass("btn-disabled");

      //Remove all the validation messaging
      this.removeValidation();

      //Get all the inputs in the Editor
      var allInputs = this.$("input, textarea, select, button");

      //Mark the disabled inputs so we can re-disable them later
      allInputs.filter(":disabled")
               .addClass("disabled-saving");

      //Remove the latest success or error alert
      this.$el.children(".alert-container").remove();

      //Disable all the inputs
      allInputs.prop("disabled", true);

    },

    /**
    *  Remove the styles set in showSaving()
    */
    hideSaving: function(){
      this.$("input, textarea, select, button")
          .prop("disabled", false);

      this.$(".disabled-saving, input.disabled")
          .prop("disabled", true)
          .removeClass("disabled-saving");

        //When the package is saved, revert the Save button back to normal
        this.$("#save-editor").html(this.submitButtonText).removeClass("btn-disabled");

    },

    /**
    * Style the view to show that it is loading
    * @param {string|DOMElement} container - The element to put the loading styling in. Either a jQuery selector or the element itself.
    * @param {string|DOMElement} message - The message to display next to the loading icon. Either a jQuery selector or the element itself.
    */
    showLoading: function(container, message){
      if(typeof container == "undefined" || !container)
        var container = this.$el;

      $(container).html(MetacatUI.appView.loadingTemplate({ msg: message }));
    },

    /**
    * Remove the styles set in showLoading()
    * @param {string|DOMElement} container - The element the loading message is conttained in. Either a jQuery selector or the element itself.
    */
    hideLoading: function(container){
      if(typeof container == "undefined" || !container)
        var container = this.$el;

      $(container).find(".loading").remove();
    },

    /**
     * Called when there is no object found with this ID
     */
    showNotFound: function(){
        //If we haven't checked the logged-in status of the user yet, wait a bit until we show a 404 msg, in case this content is their private content
        if(!MetacatUI.appUserModel.get("checked")){
          this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.showNotFound);
          return;
        }
        //If the user is not logged in
        else if(!MetacatUI.appUserModel.get("loggedIn")){
          this.showSignIn();
          return;
        }

        if(!this.model.get("notFound")) return;

        var msg = "<h4>Nothing was found for one of the following reasons:</h4>" +
          "<ul class='indent'>" +
              "<li>The ID '" + this.pid  + "' does not exist.</li>" +
            '<li>This may be private content. (Are you <a href="<%= MetacatUI.root %>/signin">signed in?</a>)</li>' +
            "<li>The content was removed because it was invalid.</li>" +
          "</ul>";

        //Remove the loading messaging
        this.hideLoading();

        //Show the not found message
        MetacatUI.appView.showAlert(msg, "alert-error", this.$("#editor-body"), null, {remove: true});

    },

    /**
    * Check the validity of this view's model
    */
    checkValidity: function(){
      if(this.model.isValid())
        this.model.trigger("valid");
    },

    /**
     * Show validation errors, if there are any
     */
    showValidation: function(){
      this.saveError("Unable to save. Either required information is missing or isn't filled out correctly.");
    },

    /**
    * Removes all the validation error styling and messaging from this view
    */
    removeValidation: function(){
      this.$(".notification.error").removeClass("error").empty();
      this.$(".validation-error-icon").hide();
    },

    /**
     * When the object is saved successfully, tell the user
     * @param {object} savedObject - the object that was successfully saved
     */
    saveSuccess: function(savedObject){

      var message = this.editorSubmitMessageTemplate({
            messageText: "Your changes have been submitted.",
            viewURL: MetacatUI.appModel.get("baseUrl"),
            buttonText: "Return home"
        });

      MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, {remove: true});

      this.hideSaving();

    },

    /**
     * When the object fails to save, tell the user
     * @param {string} errorMsg - The error message resulting from a failed attempt to save
     */
    saveError: function(errorMsg){

      var messageContainer = $(document.createElement("div")).append(document.createElement("p")),
          messageParagraph = messageContainer.find("p"),
          messageClasses = "alert-error";

      messageParagraph.append(errorMsg);

      //If the model has an error message set on it, show it in a collapseable technical details section
      if( this.model.get("errorMessage") ){
        var errorId = "error" + Math.round(Math.random()*100);
        messageParagraph.after($(document.createElement("p")).append($(document.createElement("a"))
                  .text("See technical details")
                  .attr("data-toggle", "collapse")
                  .attr("data-target", "#" + errorId)
                  .addClass("pointer")),
                $(document.createElement("div"))
                  .addClass("collapse")
                  .attr("id", errorId)
                  .append($(document.createElement("pre")).text(this.model.get("errorMessage"))));
      }

      MetacatUI.appView.showAlert(messageContainer, messageClasses, this.$el, null, {
        emailBody: errorMsg,
        remove: true
      });

      this.hideSaving();
    },

    /**
    * Shows the required icons for the sections and fields that must be completed in this editor.
    * @param {object} requiredFields - A literal object that specified which fields should be required.
    *  The keys on the object map to model attributes, and the value is true if required, false if optional.
    */
    renderRequiredIcons: function(requiredFields){

      //If no required fields are given, exit now
      if( typeof requiredFields == "undefined" ){
        return;
      }

      _.each( Object.keys(requiredFields), function(field){

        if(requiredFields[field]){
          var reqEl = this.$(".required-icon[data-category='" + field + "']");

          //Show the required icon for this category/field
          reqEl.show();

          //Show the required icon for the section
          var sectionName = reqEl.parents(".section[data-section]").attr("data-section");
          this.$(".required-icon[data-section='" + sectionName + "']").show();
        }

      }, this);
    },

    /**
    * Checks if there are unsaved changes in this Editor that should prevent closing of this view.
    * This function is also executed by the AppView, which controls the top-level navigation.
    * @returns {boolean} Returns true if this view should be closed. False if it should remain opened and active.
    */
    canClose: function(){

      //If the user isn't logged in, we can leave this view without confirmation
      if( !MetacatUI.appUserModel.get("loggedIn") )
        return true;

      //If there are no unsaved changes, we can leave this view without confirmation
      if( !this.hasUnsavedChanges() ){
        return true;
      }

      return false;

    },

    /**
    * This function is called whenever the user is about to leave this view.
    * @returns {string} The message that asks the user if they are sure they want to close this view
    */
    getConfirmCloseMessage: function(){

      //Return a confirmation message
      return "Leave this page? All of your unsaved changes will be lost.";

    },

    /**
    * Returns true if there are unsaved changes in this Editor
    * This function should be exended by each subclass of EditorView to check for unsaved changes for that model type
    * @returns {boolean}
    */
    hasUnsavedChanges: function(){
      return true;
    },

    /**
    *  Perform clean-up functions when this view is about to be removed from the page or navigated away from.
    */
    onClose: function(){

      //Remove the listener on the Window
      if( this.beforeunloadCallback ){
        window.removeEventListener("beforeunload", this.beforeunloadCallback);
        delete this.beforeunloadCallback;
      }

      //Remove the class from the body element
      $("body").removeClass("Editor rendering");

      //Remove listeners
      this.stopListening();
      this.undelegateEvents();

    }

  });

  return EditorView;
});