Source: views/portals/PortalView.js

define(["jquery",
        "underscore",
        "backbone",
        "models/portals/PortalModel",
        "text!templates/alert.html",
        "text!templates/loading.html",
        "text!templates/portals/portal.html",
        "text!templates/portals/editPortals.html",
        "views/portals/PortalHeaderView",
        "views/portals/PortalDataView",
        "views/portals/PortalSectionView",
        "views/portals/PortalMetricsView",
        "views/portals/PortalMembersView",
        "views/portals/PortalLogosView"
    ],
    function($, _, Backbone, Portal, AlertTemplate, LoadingTemplate, PortalTemplate, EditPortalsTemplate, PortalHeaderView,
        PortalDataView, PortalSectionView, PortalMetricsView, PortalMembersView, PortalLogosView) {
        "use_strict";

        /**
        * @class PortalView
        * @classdesc The PortalView is a generic view to render
         * portals, it will hold portal sections
         * @extends Backbone.View
         * @constructor
         */
        var PortalView = Backbone.View.extend(
            /** @lends PortalView.prototype */{

            /**
             * The Portal element
             * @type {string}
             */
            el: "#Content",

            /**
             * The type of View this is
             * @type {string}
             */
            type: "Portal",

            /**
             * The currently active section view
             * @type {PortalSectionView}
             */
            activeSection: undefined,

            /**
            * The currently active section label. e.g. Data, Metrics, Settings, etc.
            * @type {string}
            */
            activeSectionLabel: "",

            /**
             * The names of all sections in this portal editor
             * @type {Array}
             */
            sectionNames: [],

            /**
             * The seriesId of the portal document
             * @type {string}
             */
            portalId: "",

            /**
             * The unique short name of the portal
             * @type {string}
             */
            label: "",

            /**
            * Flag to add section name to URL. Enabled by default.
            * @type {boolean}
            */
            displaySectionInUrl: true,

            /**
             * The subviews contained within this view to be removed with onClose
             * @type {Array}
             */
            subviews: new Array(), // Could be a literal object {} */

            /**
             * A Portal Model is associated with this view and gets created during render()
             * @type {Portal}
             */
            model: null,

            /* Renders the compiled template into HTML */
            template: _.template(PortalTemplate),
            //A template to display a notification message
            alertTemplate: _.template(AlertTemplate),
            //A template for displaying a loading message
            loadingTemplate: _.template(LoadingTemplate),
            // Template for the 'edit portal' button
            editPortalsTemplate: _.template(EditPortalsTemplate),

            /**
            * A jQuery selector for the element that a single section link will be inserted into
            * @type {string}
            */
            sectionLinkContainer: ".section-link-container",
            /**
            * A jQuery selector for the elements that are links to the individual sections
            * @type {string}
            */
            sectionLinks: ".portal-section-link",
            /**
            * A jQuery selector for the section elements
            * @type {string}
            */
            sectionEls: ".portal-section-view",
            /**
             * The events this view will listen to and the associated function to call.
             * @type {Object}
             */
            events: {
              "click .portal-section-link"   : "handleSwitchSection",
              "click .section-links-container" : "toggleSectionLinks"
            },

            /**
             * Is executed when a new PortalView is created
             */
            initialize: function(options) {
                // Set the current PortalView properties
                this.portalId = options.portalId ? options.portalId : undefined;
                this.label = options.label ? options.label : undefined;
                this.activeSection = options.activeSection ? options.activeSection : undefined;
                this.activeSectionLabel = options.activeSectionLabel ? options.activeSectionLabel : undefined;
            },

            /**
             * Initial render of the PortalView
             *
             * @return {PortalView} Returns itself for easy function stacking in the app
             */
            render: function() {

                //Make sure the subviews array is reset
                this.subviews = new Array();

                // Add the overall class immediately so the navbar is styled correctly right away
                $("body").addClass("PortalView");

                this.$el.html(this.loadingTemplate({
                  msg: "Loading..."
                }));

                // Create a new Portal model
                this.model = new Portal({
                    seriesId: this.portalId,
                    label: this.label
                });

                // When the model has been synced, render the results
                this.stopListening();
                this.listenToOnce(this.model, "sync", this.renderPortal);

                //If the portal isn't found, display a 404 message
                this.listenTo(this.model, "notFound", this.handleNotFound);

                //Listen to errors that might occur during fetch()
                this.listenToOnce(this.model, "error", this.showError);

                //Fetch the model
                this.model.fetch({ objectOnly: true });

                return this;
            },

            /**
             * Render the Portal view
             */
            renderPortal: function() {


              // Add edit button if user is authorized
              this.insertOwnerControls();

              // Getting the correct portal label and seriesID
              this.label = this.model.get("label");
              this.portalId = this.model.get("seriesId");

              // Remove the listeners that were set during the fetch() process
              this.stopListening(this.model, "notFound", this.handleNotFound);
              this.stopListening(this.model, "error", this.showError);

                // Insert the overall portal template
                this.$el.html(this.template(this.model.toJSON()));

                // Render the header view
                this.headerView = new PortalHeaderView({
                    model: this.model
                });
                this.headerView.render();
                this.subviews.push(this.headerView);

                // Render the content sections
                _.each(this.model.get("sections"), function(section){
                  this.addSection(section);
                }, this);

                // Render the Data section
                if( this.model.get("hideData") !== true ) {
                    this.sectionDataView = new PortalDataView({
                        model: this.model,
                        id: "Data",
                        sectionName: "Data"
                    });
                    this.subviews.push(this.sectionDataView);

                    this.$("#portal-sections").append(this.sectionDataView.el);

                    //Render the section view and add it to the page
                    this.sectionDataView.render();

                    this.addSectionLink( this.sectionDataView );
                }

                //Render the metrics section link
                if ( this.model.get("hideMetrics") !== true ) {

                  //Create a PortalMetricsView
                  this.metricsView = new PortalMetricsView({
                    model: this.model,
                    id: this.model.get("metricsLabel"),
                    uniqueSectionName: this.model.get("metricsLabel")
                  });

                  this.subviews.push(this.metricsView);
                  this.$("#portal-sections").append(this.metricsView.el);

                  this.metricsView.render();

                  this.addSectionLink( this.metricsView );

                }

                // Render the members section
                if ( this.model.get("hideMembers") !== true &&
                     (this.model.get("associatedParties").length || this.model.get("acknowledgments"))){

                    this.sectionMembersView = new PortalMembersView({
                        model: this.model,
                        id: "Members",
                        sectionName: "Members"
                    });
                    this.subviews.push(this.sectionMembersView);

                    this.$("#portal-sections").append(this.sectionMembersView.el);

                    //Render the section view and add it to the page
                    this.sectionMembersView.render();

                    this.addSectionLink( this.sectionMembersView );
                }

                //Switch to the active section
                this.switchSection();

                //Render the logos at the bottom of the portal page
                var ackLogos = this.model.get("acknowledgmentsLogos") || [];
                this.logosView = new PortalLogosView();
                this.logosView.logos = ackLogos;
                this.subviews.push(this.logosView);
                this.logosView.render();
                this.$(".portal-view").append(this.logosView.el);

                //Scroll to an inner-page link if there is one specified
                if( window.location.hash && this.$(window.location.hash).length ){
                  MetacatUI.appView.scrollTo(this.$(window.location.hash));
                }


                // Save reference to this view
                var view = this;

                // On mobile, hide section tabs a moment after page loads so
                // users notice where they are
                setTimeout(function () {
                  view.toggleSectionLinks();
                }, 700);

                // On mobile where the section-links-container is set to fixed,
                // hide the portal navigation element when user scrolls down,
                // show again when the user scrolls up.
                MetacatUI.appView.prevScrollpos = window.pageYOffset;
                $(window).on("scroll", "", undefined, this.handleScroll);

            },

            /**
             * toggleSectionLinks - show or hide the section links nav. Used for
             * mobile/small screens only.
             */
            toggleSectionLinks: function(){
              try{
                // Only toggle the section links on mobile. On mobile, the
                // ".show-sections-toggle" is visible.
                if(this.$(".show-sections-toggle").is(":visible")){
                  this.$("#portal-section-tabs").slideToggle();
                }
              } catch(e){
                console.log("Failed to toggle section links, error message: " + e);
              }
            },

            /*
             * Checks the authority for the logged in user for this portal and
             * inserts control elements onto the page for the user to interact
             * with the portal. So far, this is just an 'edit portal' button.
             */
            insertOwnerControls: function(){

              // Insert the button into the navbar
              var container = $(".edit-portal-link-container");

              var model = this.model;

              this.listenToOnce(this.model, "change:isAuthorized", function(){
                if(!model.get("isAuthorized")){
                  return false;
                } else {
                  container.html(
                    this.editPortalsTemplate({
                      editButtonText: "Edit " + MetacatUI.appModel.get('portalTermSingular'),
                      pathToEdit: MetacatUI.root + "/edit/"+ MetacatUI.appModel.get("portalTermPlural") +"/" + model.get("label")
                    })
                  );
                }
              });

              this.model.checkAuthority("write");
            },

            /**
             * Update the window location path with the active section name
             * @param {boolean} [showSectionLabel] - If true, the section label will be added to the path
            */
            updatePath: function(showSectionLabel){

              var label         = this.model.get("label") || this.newPortalTempName,
                  originalLabel = this.model.get("originalLabel") || this.newPortalTempName,
                  pathName      = decodeURIComponent(window.location.pathname)
                                  .substring(MetacatUI.root.length)
                                  // remove trailing forward slash if one exists in path
                                  .replace(/\/$/, "");

              // Add or replace the label and section part of the path with updated values.
              // pathRE matches "/label/section", where the "/section" part is optional
              var pathRE = new RegExp("\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$", "i");
              newPathName = pathName.replace(pathRE, "") + "/" + label;

              if( showSectionLabel && this.activeSection ){
                newPathName += "/" + this.activeSection.uniqueSectionLabel;
              }

              // Update the window location
              MetacatUI.uiRouter.navigate( newPathName, { trigger: false } );

            },

            /**
             * Gets a list of section names from tab elements and updates the
             * sectionNames attribute on this view.
             */
            updateSectionNames: function() {

              // Get the section names from the tab elements
              var sectionNames = [];
              this.$(this.sectionLinks)
                .each(function(i, anchorEl){
                  sectionNames[i] = $(anchorEl)
                                      .attr("href")
                                      .substring(1)
                });

              // Set the array of sectionNames on the view
              this.sectionNames = sectionNames
            },

            /**
            * Manually switch to a section subview by making the tab and tab panel active.
            * Navigation between sections is usually handled automatically by the Bootstrap
            * library, but a manual switch may be necessary sometimes
            * @param {PortalSectionView} [sectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view.
            */
            switchSection: function(sectionView){

              //Create a flag for whether the section label should be shown in the URL
              var showSectionLabelInURL = true;

              // If no section view is given, use the active section in the view.
              if( !sectionView ){
                //Use the sectionView set already
                if( this.activeSection ){
                  var sectionView = this.activeSection;
                }
                //Or find the section view by name, which may have been passed through the URL
                else if( this.activeSectionLabel ){
                  var sectionView = this.getSectionByLabel(this.activeSectionLabel);
                }
              }

              //If no section view was indicated, just default to the first visible one
              if( !sectionView ){
                var sectionView = this.$(this.sectionLinkContainer).first().data("view");

                //If we are defaulting to the first section, don't show the section label in the URL
                showSectionLabelInURL = false;

                //If there are no section views on the page at all, exit now
                if( !sectionView ){
                  return;
                }
              }

              // Update the activeSection set on the view
              this.activeSection = sectionView;


              // Activate the section content
              this.$(this.sectionEls).each(function(i, contentEl){
                if($(contentEl).data("view") == sectionView){
                  $(contentEl).addClass("active");
                } else {
                  // make sure no other sections are active
                  $(contentEl).removeClass("active");
                }
              });

              // Activate the link to the content
              this.$(this.sectionLinkContainer).each(function(i, linkEl){
                if( $(linkEl).data("view") == sectionView ){
                  $(linkEl).addClass("active")
                } else {
                  // make sure no other sections are active
                  $(linkEl).removeClass("active")
                };
              });

              //If the section view has post-render functionality, execute it now
              if( typeof sectionView.postRender == "function" ){
                sectionView.postRender();
              }

              //Update the location path with the new section name
              this.updatePath(showSectionLabelInURL);

            },

            /**
            * When a section link has been clicked, switch to that section
            * @param {Event} e - The click event on the section link
            */
            handleSwitchSection: function(e){

              e.preventDefault();

              var sectionView = $(e.target).parents(this.sectionLinkContainer).first().data("view");

              if( sectionView ){
                this.switchSection(sectionView);

                // If the user clicks a link and is not near the top of the page
                // (i.e. on mobile), scroll to the top of the section content.
                // Otherwise it might look like the page hasn't changed (e.g.
                // when focus is on the footer)
                if(window.pageYOffset > this.$("#portal-sections").offset().top){
                  MetacatUI.appView.scrollTo(this.$("#portal-sections"));
                }

              }

            },

            /**
            * Returns the section view that has a label matching the one given.
            * @param {string} label - The label for the section
            * @return {PortalSectionView|false} - Returns false if a matching section view isn't found
            */
            getSectionByLabel: function(label){

              //If no label is given, exit
              if(!label){
                return;
              }

              //Find the section view whose unique label matches the given label. Case-insensitive matching.
              return _.find( this.subviews, function(view){
                if( typeof view.uniqueSectionLabel == "string" ){
                  return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase();
                }
                else{
                  return false;
                }
              });
            },

            /**
            * Creates and returns a unique label for the given section. This label is just used in the view,
            * because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view.
            * @param {PortEditorSection} sectionModel - The section for which to create a unique label
            * @return {string} The unique label string
            */
            getUniqueSectionLabel: function(sectionModel){
              //Get the label for this section
              var sectionLabel = sectionModel.get("label").replace(/[^a-zA-Z0-9 ]/g, "").replace(/ /g, "-"),
                  unalteredLabel = sectionLabel,
                  sectionLabels = this.sectionLabels || [],
                  i = 2;

              //Concatenate a number to the label if this one already exists
              while( sectionLabels.includes(sectionLabel) ){
                sectionLabel = unalteredLabel + i;
                i++;
              }

              return sectionLabel;
            },

            /**
             * Creates a PortalSectionView to display the content in the given portal
             * section. Also creates a navigation link to the section.
             *
             * @param {PortalSectionModel} sectionModel - The section to render in this view
             */
            addSection: function(sectionModel){

              //Create a new PortalSectionView
              var sectionView = new PortalSectionView({
                model: sectionModel
              });

              //Render the section
              sectionView.render();

              //Add the section view to this portal view
              this.$("#portal-sections").append(sectionView.el);

              this.addSectionLink( sectionView );

              //Create a unique label for this section and save it
              var uniqueLabel = this.getUniqueSectionLabel(sectionModel);
              //Set the unique section label for this view
              sectionView.uniqueSectionLabel = uniqueLabel;

              this.subviews.push(sectionView);

            },

            /**
             * Add a link to a section of this portal page
             * @param {PortalSectionView} sectionView - The view to add a link to
             */
            addSectionLink: function(sectionView){

              var label = sectionView.getName();
              var hrefLabel = sectionView.getName({ linkFriendly: true });

              //Create a navigation link
              this.$("#portal-section-tabs").append(
                $(document.createElement("li"))
                  .addClass("section-link-container")
                  .data("view", sectionView)
                  .append( $(document.createElement("a"))
                             .text(label)
                             .attr("href", "#" + hrefLabel )
                             .attr("data-toggle", "tab")
                             .addClass("portal-section-link")
                             .data("view", sectionView)));

            },

            /**
            * Handles the case where the PortalModel is fetched and nothing is found.
            */
            handleNotFound: function(){

              //If the user is NOT logged in OR
              // if the suer is logged in, and the last fetch was done with user credentials, then this Portal is either not accessible or non-existent
              if( MetacatUI.appUserModel.get("checked") && !MetacatUI.appUserModel.get("loggedIn") ||
                  (MetacatUI.appUserModel.get("checked") && MetacatUI.appUserModel.get("loggedIn") && this.model.get("fetchedWithAuth")) ){

                var view = this;

                //Check if there is an indexing queue, because this model may still be indexing
                var onError = function(){
                    //If the request to the monitor/status API fails, then show the not-found message
                    view.showNotFound.call(view);
                  },
                  onSuccess = function(sizeOfQueue){

                    if( sizeOfQueue > 0 ){
                      //Show a warning message about the index queue
                      MetacatUI.appView.showAlert(
                        "<p>We couldn't find a data portal named \"" + (view.label || view.portalId) +
                          "\".</p><p><i class='icon icon-exclamation-sign'></i> If this portal was created in the last few minutes, it may still be processing, since there are currently <b>" + sizeOfQueue +
                          "</b> submissions in the queue.</p>",
                        "alert-warning",
                        view.$el
                      );
                      view.$(".loading").remove();
                    }
                    else{
                      //If the size of the queue is 0, then show the not-found message
                      view.showNotFound.call(view);
                    }

                  }

                //Get the size of the index queue
                MetacatUI.appLookupModel.getSizeOfIndexQueue(onSuccess, onError);

              }
              //If the user IS logged in and we haven't fetched the model with user authentication yet
              else if( MetacatUI.appUserModel.get("checked") && MetacatUI.appUserModel.get("loggedIn") ){
                //Fetch again now that the user is logged in
                this.model.fetch();
              }
              //If the user login status is unknown, because authentication is still pending
              else if( !MetacatUI.appUserModel.get("checked") ){
                //Wait for the authentication to be checked, and then start this function over again
                this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.handleNotFound);
              }

            },

            /**
             * If the given portal doesn't exist, display a Not Found message.
             */
            showNotFound: function(){

              var notFoundMessage = "The data portal \"" + (this.label || this.portalId) +
                                    "\" doesn't exist.",
                  notification = this.alertTemplate({
                    classes: "alert-error",
                    msg: notFoundMessage,
                    includeEmail: true
                  });

              this.$el.html(notification);

            },

            /**
            * Show an error message in this view
            * @param {SolrResult} model
            * @param {XMLHttpRequest.response} response
            */
            showError: function(model, response){

              var errorMsg = "";
              if( response && response.responseText ){
                errorMsg = "<p>Error details: " + $(response.responseText).text() + "</p>";
              }

              //Show the error message
              MetacatUI.appView.showAlert(
                "<h4><i class='icon icon-frown'></i>Something went wrong while displaying this portal.</h4>" + errorMsg,
                "alert-error",
                this.$el
              );

              //Remove the loading message from this view
              this.$el.find(".loading").remove();

            },

            /**
            * This function is called whenever the window is scrolled.
            */
            handleScroll: function() {
              var menu = $(".section-links-container")[0],
                  menuHeight = $(menu).height(),
                  hiddenHeight = (menuHeight * -1);
              var currentScrollPos = window.pageYOffset;
              if(MetacatUI.appView.prevScrollpos > currentScrollPos) {
                menu.style.bottom = "0px";
              } else {
                menu.style.bottom = hiddenHeight +"px";
              }
              MetacatUI.appView.prevScrollpos = currentScrollPos;
            },

            /**
             * This function is called when the app navigates away from this view.
             * Any clean-up or housekeeping happens at this time.
             */
            onClose: function() {
                //Remove each subview from the DOM and remove listeners
                _.invoke(this.subviews, "remove");

                this.subviews = new Array();

                //Remove all listeners
                this.stopListening();

                //Delete the metrics view from this view
                delete this.sectionMetricsView;
                //Delete the model from this view
                delete this.model;

                //Remove the scroll listener
                $(window).off("scroll", "", this.handleScroll);

                $("body").removeClass("PortalView");

                $("#editPortal").remove();

                this.undelegateEvents();
            }
        });

        return PortalView;
    });