Source: src/js/routers/router.js

"use strict";

define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
  /**
   * @class UIRouter
   * @classdesc MetacatUI Router
   * @classcategory Router
   * @extends Backbone.Router
   * @constructor
   */
  var UIRouter = Backbone.Router.extend(
    /** @lends UIRouter.prototype */ {
      routes: {
        "": "renderIndex", // the default route
        "about(/:anchorId)(/)": "renderAbout", // about page
        "help(/:page)(/:anchorId)(/)": "renderHelp",
        "tools(/:anchorId)(/)": "renderTools", // tools page
        "data/my-data(/page/:page)(/)": "renderMyData", // data search page
        "data(/mode=:mode)(/query=:query)(/page/:page)(/)": "renderData", // data search page
        "data/my-data(/)": "renderMyData",
        "profile(/*username)(/s=:section)(/s=:subsection)(/)": "renderProfile",
        "my-profile(/s=:section)(/s=:subsection)(/)": "renderMyProfile",
        "logout(/)": "logout", // logout the user
        "signout(/)": "logout", // logout the user
        "signin(/)": "renderSignIn", // signin the user
        "signinsuccess(/)": "renderSignInSuccess",
        "signinldaperror(/)": "renderLdapSignInError",
        "signinLdap(/)": "renderLdapSignIn",
        "signinSuccessLdap(/)": "renderLdapSignInSuccess",
        "signin-help": "renderSignInHelp", //The Sign In troubleshotting page
        "share(/*pid)(/)": "renderEditor", // registry page
        "submit(/*pid)(/)": "renderEditor", // registry page
        "quality(/s=:suiteId)(/:pid)(/)": "renderMdqRun", // MDQ page
        "api(/:anchorId)(/)": "renderAPI", // API page
        "edit/:portalTermPlural(/:portalIdentifier)(/:portalSection)(/)":
          "renderPortalEditor",
        drafts: "renderDrafts",
      },

      helpPages: {
        search: "searchTips",
        defaultPage: "searchTips",
      },

      initialize: function () {
        // Add routes to portal dynamically using the appModel portal term
        var portalTermPlural = MetacatUI.appModel.get("portalTermPlural");
        this.route(
          portalTermPlural + "(/:portalId)(/:portalSection)(/)",
          ["portalId", "portalSection"],
          this.renderPortal,
        );

        this.listenTo(
          Backbone.history,
          "routeNotFound",
          this.navigateToDefault,
        );

        // This route handler replaces the route handler we had in the
        // routes table before which was "view/*pid". The * only finds URL
        // parts until the ? but DataONE PIDs can have ? in them so we need
        // to make this route more inclusive.
        this.route(/^view\/(.*)$/, "renderMetadata");

        this.on("route", this.trackPathName);

        // Clear stale JSONLD and meta tags
        this.on("route", this.clearJSONLD);
        this.on("route", this.clearHighwirePressMetaTags);
      },

      //Keep track of navigation movements
      routeHistory: new Array(),
      pathHistory: new Array(),

      // Will return the last route, which is actually the second to last item in the route history,
      // since the last item is the route being currently viewed
      lastRoute: function () {
        if (
          typeof this.routeHistory === "undefined" ||
          this.routeHistory.length <= 1
        )
          return false;
        else return this.routeHistory[this.routeHistory.length - 2];
      },

      trackPathName: function (e) {
        if (_.last(this.pathHistory) != window.location.pathname)
          this.pathHistory.push(window.location.pathname);
      },

      //If the user or app cancelled the last route, call this function to revert
      // the window location pathname back to the correct value
      undoLastRoute: function () {
        this.routeHistory.pop();

        // Remove the last route and pathname from the history
        if (_.last(this.pathHistory) == window.location.pathname)
          this.pathHistory.pop();

        //Change the pathname in the window location back
        this.navigate(_.last(this.pathHistory), { replace: true });
      },

      renderIndex: function (param) {
        this.routeHistory.push("index");

        if (!MetacatUI.appView.indexView) {
          require(["views/IndexView"], function (IndexView) {
            MetacatUI.appView.indexView = new IndexView();
            MetacatUI.appView.showView(MetacatUI.appView.indexView);
          });
        } else MetacatUI.appView.showView(MetacatUI.appView.indexView);
      },

      renderText: function (options) {
        if (!MetacatUI.appView.textView) {
          require(["views/TextView"], function (TextView) {
            MetacatUI.appView.textView = new TextView();
            MetacatUI.appView.showView(MetacatUI.appView.textView, options);
          });
        } else MetacatUI.appView.showView(MetacatUI.appView.textView, options);
      },

      renderHelp: function (page, anchorId) {
        this.routeHistory.push("help");
        MetacatUI.appModel.set("anchorId", anchorId);

        if (page) var pageName = this.helpPages[page];
        else var pageName = this.helpPages["defaultPage"]; //default

        var options = {
          pageName: pageName,
          anchorId: anchorId,
        };

        this.renderText(options);
      },

      renderSignInHelp: function () {
        this.routeHistory.push("signin-help");
        this.renderText({ pageName: "signInHelp" });
      },

      renderAbout: function (anchorId) {
        this.routeHistory.push("about");
        MetacatUI.appModel.set("anchorId", anchorId);
        var options = {
          pageName: "about",
          anchorId: anchorId,
        };

        this.renderText(options);
      },

      renderAPI: function (anchorId) {
        this.routeHistory.push("api");
        MetacatUI.appModel.set("anchorId", anchorId);
        var options = {
          pageName: "api",
          anchorId: anchorId,
        };

        this.renderText(options);
      },

      renderProjects: function () {
        require(["views/projects/ProjectView"], function (ProjectView) {
          MetacatUI.appView.projectView = new ProjectView();
          MetacatUI.appView.showView(MetacatUI.appView.projectView);
        });
      },

      /*
       * Renders the editor view given a root package identifier,
       * or a metadata identifier.  If the latter, the corresponding
       * package identifier will be queried and then rendered.
       */
      renderEditor: function (pid) {
        //If there is no EML211EditorView yet, create one
        if (!MetacatUI.appView.eml211EditorView) {
          var router = this;

          //Load the EML211EditorView file
          require(["views/metadata/EML211EditorView"], function (
            EML211EditorView,
          ) {
            //Add the submit route to the router history
            router.routeHistory.push("submit");

            //Create a new EML211EditorView
            MetacatUI.appView.eml211EditorView = new EML211EditorView({
              pid: pid,
            });

            //Set the pid from the pid given in the URL
            MetacatUI.appView.eml211EditorView.pid = pid;

            //Render the EML211EditorView
            MetacatUI.appView.showView(MetacatUI.appView.eml211EditorView);
          });
        } else {
          //Set the pid from the pid given in the URL
          MetacatUI.appView.eml211EditorView.pid = pid;

          //Add the submit route to the router history
          this.routeHistory.push("submit");

          //Render the Editor View
          MetacatUI.appView.showView(MetacatUI.appView.eml211EditorView);
        }
      },

      /**
       * Renders the Drafts view which is a simple view backed by LocalForage that
       * lists drafts created in the Editor so users can recover any failed
       * submissions.
       */
      renderDrafts: function () {
        require(["views/DraftsView"], function (DraftsView) {
          MetacatUI.appView.draftsView = new DraftsView();
          MetacatUI.appView.showView(MetacatUI.appView.draftsView);
        });
      },

      renderMdqRun: function (suiteId, pid) {
        this.routeHistory.push("quality");

        if (!MetacatUI.appView.mdqRunView) {
          require(["views/MdqRunView"], function (MdqRunView) {
            MetacatUI.appView.mdqRunView = new MdqRunView();
            MetacatUI.appView.mdqRunView.suiteId = suiteId;
            MetacatUI.appView.mdqRunView.pid = pid;
            MetacatUI.appView.showView(MetacatUI.appView.mdqRunView);
          });
        } else {
          MetacatUI.appView.mdqRunView.suiteId = suiteId;
          MetacatUI.appView.mdqRunView.pid = pid;
          MetacatUI.appView.showView(MetacatUI.appView.mdqRunView);
        }
      },

      renderTools: function (anchorId) {
        this.routeHistory.push("tools");
        MetacatUI.appModel.set("anchorId", anchorId);

        var options = {
          pageName: "tools",
          anchorId: anchorId,
        };

        this.renderText(options);
      },

      renderMyData: function (page) {
        //Only display this is the user is logged in
        if (
          !MetacatUI.appUserModel.get("loggedIn") &&
          MetacatUI.appUserModel.get("checked")
        )
          this.navigate("data", { trigger: true });
        else if (!MetacatUI.appUserModel.get("checked")) {
          var router = this;

          this.listenToOnce(
            MetacatUI.appUserModel,
            "change:checked",
            function () {
              if (MetacatUI.appUserModel.get("loggedIn"))
                router.renderMyData(page);
              else this.navigate("data", { trigger: true });
            },
          );

          return;
        }

        this.routeHistory.push("data");

        ///Check for a page URL parameter
        if (typeof page === "undefined") MetacatUI.appModel.set("page", 0);
        else MetacatUI.appModel.set("page", page);

        if (!MetacatUI.appView.dataCatalogView) {
          require(["views/DataCatalogView"], function (DataCatalogView) {
            MetacatUI.appView.dataCatalogView = new DataCatalogView();
            MetacatUI.appView.dataCatalogView.searchModel =
              MetacatUI.appUserModel.get("searchModel").clone();
            MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
          });
        } else {
          MetacatUI.appView.dataCatalogView.searchModel = MetacatUI.appUserModel
            .get("searchModel")
            .clone();
          MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
        }
      },

      renderData: function (mode, query, page) {
        this.routeHistory.push("data");
        // Check for a page URL parameter
        page = parseInt(page);
        page = isNaN(page) || page < 1 ? 1 : page;
        MetacatUI.appModel.set("page", page - 1);

        // Check if we are using the new CatalogSearchView
        if (!MetacatUI.appModel.get("useDeprecatedDataCatalogView")) {
          require(["views/search/CatalogSearchView"], function (
            CatalogSearchView,
          ) {
            MetacatUI.appView.catalogSearchView = new CatalogSearchView({
              initialQuery: query,
            });
            MetacatUI.appView.showView(MetacatUI.appView.catalogSearchView);
          });
          return;
        }

        // Check for a query URL parameter
        if (typeof query !== "undefined" && query) {
          MetacatUI.appSearchModel.set("additionalCriteria", [query]);
        }

        require(["views/DataCatalogView"], function (DataCatalogView) {
          if (!MetacatUI.appView.dataCatalogView) {
            MetacatUI.appView.dataCatalogView = new DataCatalogView();
          }
          if (mode) MetacatUI.appView.dataCatalogView.mode = mode;
          MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
        });
      },

      /**
       * Renders the Portals Search view.
       */
      renderPortalsSearch: function () {
        require(["views/portals/PortalsSearchView"], function (
          PortalsSearchView,
        ) {
          MetacatUI.appView.showView(new PortalsSearchView({ el: "#Content" }));
        });
      },

      /**
       * renderPortal - Render the portal view based on the given name or id, as
       * well as optional section
       *
       * @param  {string} label         The portal ID or name
       * @param  {string} portalSection A specific section within the portal
       */
      renderPortal: function (label, portalSection) {
        //If no portal was specified, go to the portal search view
        if (!label) {
          this.renderPortalsSearch();
          return;
        }

        // Add the overall class immediately so the navbar is styled correctly right away
        $("body").addClass("PortalView");
        // Look up the portal document seriesId by its registered name if given
        if (portalSection) {
          this.routeHistory.push(
            MetacatUI.appModel.get("portalTermPlural") +
              "/" +
              label +
              "/" +
              portalSection,
          );
        } else {
          this.routeHistory.push(
            MetacatUI.appModel.get("portalTermPlural") + "/" + label,
          );
        }

        require(["views/portals/PortalView"], function (PortalView) {
          MetacatUI.appView.portalView = new PortalView({
            label: label,
            activeSectionLabel: portalSection,
          });
          MetacatUI.appView.showView(MetacatUI.appView.portalView);
        });
      },

      /**
       * Renders the PortalEditorView
       * @param {string} [portalTermPlural] - This should match the `portalTermPlural` configured in the AppModel.
       * @param {string} [portalIdentifier] - The id or labebl of the portal
       * @param {string} [portalSection] - The name of the section within the portal to navigate to (e.g. "data")
       */
      renderPortalEditor: function (
        portalTermPlural,
        portalIdentifier,
        portalSection,
      ) {
        //If the user navigated to a route with a portal term other than the one supported, then this is not a portal editor route.
        if (portalTermPlural != MetacatUI.appModel.get("portalTermPlural")) {
          this.navigateToDefault();
          return;
        }

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

        // Look up the portal document seriesId by its registered name if given
        if (portalSection) {
          this.routeHistory.push(
            "edit/" +
              MetacatUI.appModel.get("portalTermPlural") +
              "/" +
              portalIdentifier +
              "/" +
              portalSection,
          );
        } else {
          if (!portalIdentifier) {
            this.routeHistory.push(
              "edit/" + MetacatUI.appModel.get("portalTermPlural"),
            );
          } else {
            this.routeHistory.push(
              "edit/" +
                MetacatUI.appModel.get("portalTermPlural") +
                "/" +
                portalIdentifier,
            );
          }
        }

        require(["views/portals/editor/PortalEditorView"], function (
          PortalEditorView,
        ) {
          MetacatUI.appView.portalEditorView = new PortalEditorView({
            activeSectionLabel: portalSection,
            portalIdentifier: portalIdentifier,
          });
          MetacatUI.appView.showView(MetacatUI.appView.portalEditorView);
        });
      },

      renderMetadata: function (pid) {
        pid = decodeURIComponent(pid);

        this.routeHistory.push("metadata");
        MetacatUI.appModel.set("lastPid", MetacatUI.appModel.get("pid"));

        var seriesId;

        //Check for a seriesId
        if (pid.indexOf("version:") > -1) {
          seriesId = pid.substr(0, pid.indexOf(", version:"));

          pid = pid.substr(pid.indexOf(", version: ") + ", version: ".length);
        }

        //Save the id in the app model
        MetacatUI.appModel.set("pid", pid);

        if (!MetacatUI.appView.metadataView) {
          require(["views/MetadataView"], function (MetadataView) {
            MetacatUI.appView.metadataView = new MetadataView();

            //Send the id(s) to the view
            MetacatUI.appView.metadataView.seriesId = seriesId;
            MetacatUI.appView.metadataView.pid = pid;

            MetacatUI.appView.showView(MetacatUI.appView.metadataView);
          });
        } else {
          //Send the id(s) to the view
          MetacatUI.appView.metadataView.seriesId = seriesId;
          MetacatUI.appView.metadataView.pid = pid;

          // MetacatUI resets the dataPackage and dataPackageSynced
          // attributes before rendering the view. These attributes are
          // initialized on a per-dataset basis to prevent displaying the
          // same dataset repeatedly.

          MetacatUI.appView.metadataView.dataPackage = null;
          MetacatUI.appView.metadataView.dataPackageSynced = false;

          MetacatUI.appView.showView(MetacatUI.appView.metadataView);
        }
      },

      renderProfile: function (username, section, subsection) {
        this.closeLastView();

        var viewChoice;

        //If there is a username specified and user profiles are disabled,
        // forward to the entire repo profile view.
        if (username && !MetacatUI.appModel.get("enableUserProfiles")) {
          this.navigate("profile", { trigger: true, replace: true });
          return;
        }

        if (!username) {
          this.routeHistory.push("summary");

          // flag indicating /profile view
          var viewOptions = { nodeSummaryView: true };

          if (!MetacatUI.appView.statsView) {
            require(["views/StatsView"], function (StatsView) {
              MetacatUI.appView.statsView = new StatsView({
                userType: "repository",
              });

              MetacatUI.appView.showView(
                MetacatUI.appView.statsView,
                viewOptions,
              );
            });
          } else
            MetacatUI.appView.showView(
              MetacatUI.appView.statsView,
              viewOptions,
            );
        } else {
          this.routeHistory.push("profile");
          MetacatUI.appModel.set("profileUsername", username);

          if (section || subsection) {
            var viewOptions = { section: section, subsection: subsection };
          }

          if (!MetacatUI.appView.userView) {
            require(["views/UserView"], function (UserView) {
              MetacatUI.appView.userView = new UserView();

              MetacatUI.appView.showView(
                MetacatUI.appView.userView,
                viewOptions,
              );
            });
          } else
            MetacatUI.appView.showView(MetacatUI.appView.userView, viewOptions);
        }
      },

      renderMyProfile: function (section, subsection) {
        if (
          MetacatUI.appUserModel.get("checked") &&
          !MetacatUI.appUserModel.get("loggedIn")
        )
          this.renderSignIn();
        else if (!MetacatUI.appUserModel.get("checked")) {
          this.listenToOnce(
            MetacatUI.appUserModel,
            "change:checked",
            function () {
              if (MetacatUI.appUserModel.get("loggedIn"))
                this.renderProfile(
                  MetacatUI.appUserModel.get("username"),
                  section,
                  subsection,
                );
              else this.renderSignIn();
            },
          );
        } else if (
          MetacatUI.appUserModel.get("checked") &&
          MetacatUI.appUserModel.get("loggedIn")
        ) {
          this.renderProfile(
            MetacatUI.appUserModel.get("username"),
            section,
            subsection,
          );
        }
      },

      logout: function (param) {
        //Clear our browsing history when we log out
        this.routeHistory.length = 0;

        if (
          (typeof MetacatUI.appModel.get("tokenUrl") == "undefined" ||
            !MetacatUI.appModel.get("tokenUrl")) &&
          !MetacatUI.appView.registryView
        ) {
          require(["views/RegistryView"], function (RegistryView) {
            MetacatUI.appView.registryView = new RegistryView();
            if (MetacatUI.appView.currentView.onClose)
              MetacatUI.appView.currentView.onClose();
            MetacatUI.appUserModel.logout();
          });
        } else {
          if (
            MetacatUI.appView.currentView &&
            MetacatUI.appView.currentView.onClose
          )
            MetacatUI.appView.currentView.onClose();
          MetacatUI.appUserModel.logout();
        }
      },

      renderSignIn: function () {
        var router = this;

        //If there is no SignInView yet, create one
        if (!MetacatUI.appView.signInView) {
          require(["views/SignInView"], function (SignInView) {
            MetacatUI.appView.signInView = new SignInView({
              el: "#Content",
              fullPage: true,
            });
            router.renderSignIn();
          });

          return;
        }

        //If the user status has been checked and they are already logged in, we will forward them to their profile
        if (
          MetacatUI.appUserModel.get("checked") &&
          MetacatUI.appUserModel.get("loggedIn")
        ) {
          this.navigate("my-profile", { trigger: true });
          return;
        }
        //If the user status has been checked and they are NOT logged in, show the SignInView
        else if (
          MetacatUI.appUserModel.get("checked") &&
          !MetacatUI.appUserModel.get("loggedIn")
        ) {
          this.routeHistory.push("signin");
          MetacatUI.appView.showView(MetacatUI.appView.signInView);
        }
        //If the user status has not been checked yet, wait for it
        else if (!MetacatUI.appUserModel.get("checked")) {
          this.listenToOnce(
            MetacatUI.appUserModel,
            "change:checked",
            this.renderSignIn,
          );
          MetacatUI.appView.showView(MetacatUI.appView.signInView);
        }
      },

      renderSignInSuccess: function () {
        $("body").html("Sign-in successful.");
        setTimeout(window.close, 1000);
      },

      renderLdapSignInSuccess: function () {
        //If there is an LDAP sign in error message
        if (
          window.location.pathname.indexOf(
            "error=Unable%20to%20authenticate%20LDAP%20user",
          ) > -1
        ) {
          this.renderLdapOnlySignInError();
        } else {
          this.renderSignInSuccess();
        }
      },

      renderLdapSignInError: function () {
        this.routeHistory.push("signinldaperror");

        if (!MetacatUI.appView.signInView) {
          require(["views/SignInView"], function (SignInView) {
            MetacatUI.appView.signInView = new SignInView({ el: "#Content" });
            MetacatUI.appView.signInView.ldapError = true;
            MetacatUI.appView.signInView.ldapOnly = true;
            MetacatUI.appView.signInView.fullPage = true;
            MetacatUI.appView.showView(MetacatUI.appView.signInView);
          });
        } else {
          MetacatUI.appView.signInView.ldapError = true;
          MetacatUI.appView.signInView.ldapOnly = true;
          MetacatUI.appView.signInView.fullPage = true;
          MetacatUI.appView.showView(MetacatUI.appView.signInView);
        }
      },

      renderLdapOnlySignInError: function () {
        this.routeHistory.push("signinldaponlyerror");

        if (!MetacatUI.appView.signInView) {
          require(["views/SignInView"], function (SignInView) {
            var signInView = new SignInView({ el: "#Content" });
            signInView.ldapError = true;
            signInView.ldapOnly = true;
            signInView.fullPage = true;
            MetacatUI.appView.showView(signInView);
          });
        } else {
          var signInView = new SignInView({ el: "#Content" });
          signInView.ldapError = true;
          signInView.ldapOnly = true;
          signInView.fullPage = true;
          MetacatUI.appView.showView(signInView);
        }
      },

      renderLdapSignIn: function () {
        this.routeHistory.push("signinLdap");

        if (!MetacatUI.appView.signInView) {
          require(["views/SignInView"], function (SignInView) {
            MetacatUI.appView.signInView = new SignInView({ el: "#Content" });
            MetacatUI.appView.signInView.ldapOnly = true;
            MetacatUI.appView.signInView.fullPage = true;
            MetacatUI.appView.showView(MetacatUI.appView.signInView);
          });
        } else {
          var signInLdapView = new SignInView({ el: "#Content" });
          MetacatUI.appView.signInView.ldapOnly = true;
          MetacatUI.appView.signInView.fullPage = true;
          MetacatUI.appView.showView(signInLdapView);
        }
      },

      navigateToDefault: function () {
        //Navigate to the default view
        this.navigate(MetacatUI.appModel.defaultView, { trigger: true });
      },

      /*
       * Gets an array of route names that are set on this router.
       * @return {Array} - An array of route names, not including any special characters
       */
      getRouteNames: function () {
        var router = this;

        var routeNames = _.map(Object.keys(this.routes), function (routeName) {
          return router.getRouteName(routeName);
        });

        //The "view" and portals routes are not included in the route hash (they are set up during initialize),
        // so we have to manually add it here.
        routeNames.push("view");
        if (!routeNames.includes(MetacatUI.appModel.get("portalTermPlural"))) {
          routeNames.push(MetacatUI.appModel.get("portalTermPlural"));
        }

        return routeNames;
      },

      /*
       * Gets the route name based on the route pattern given
       * @param {string} routePattern - A string that represents the route pattern e.g. "view(/pid)"
       * @return {string} - The name of the route without any pattern special characters e.g. "view"
       */
      getRouteName: function (routePattern) {
        var specialChars = ["/", "(", "*", ":"];

        _.each(specialChars, function (specialChar) {
          var substring = routePattern.substring(
            0,
            routePattern.indexOf(specialChar),
          );

          if (substring && substring.length < routePattern.length) {
            routePattern = substring;
          }
        });

        return routePattern;
      },

      closeLastView: function () {
        //Get the last route and close the view
        var lastRoute = _.last(this.routeHistory);

        if (lastRoute == "summary") MetacatUI.appView.statsView.onClose();
        else if (lastRoute == "profile") MetacatUI.appView.userView.onClose();
      },

      clearJSONLD: function () {
        $("#jsonld").remove();
      },

      clearHighwirePressMetaTags: function () {
        $("head > meta[name='citation_title']").remove();
        $("head > meta[name='citation_authors']").remove();
        $("head > meta[name='citation_publisher']").remove();
        $("head > meta[name='citation_date']").remove();
      },
    },
  );

  return UIRouter;
});