Source: src/js/views/maps/viewfinder/ViewfinderView.js

"use strict";

define([
  "underscore",
  "backbone",
  "text!templates/maps/viewfinder/viewfinder.html",
  "views/maps/viewfinder/SearchView",
  "views/maps/viewfinder/ViewfinderCardsListView",
  "views/maps/ExpansionPanelView",
  "models/maps/ExpansionPanelsModel",
  "models/maps/viewfinder/ViewfinderModel",
], (
  _,
  Backbone,
  Template,
  SearchView,
  ViewfinderCardsListView,
  ExpansionPanelView,
  ExpansionPanelsModel,
  ViewfinderModel,
) => {
  // The base classname to use for this View's template elements.
  const BASE_CLASS = "viewfinder";
  // The HTML classes to use for this view's HTML elements.
  const CLASS_NAMES = {
    searchView: `${BASE_CLASS}__search`,
    viewfinderCardsView: `${BASE_CLASS}__cards`,
  };

  /**
   * @class ViewfinderView
   * @classdesc ViewfinderView allows a user to search for
   * a latitude and longitude in the map view, and find suggestions
   * for places related to their search terms.
   * @classcategory Views/Maps
   * @name ViewfinderView
   * @augments Backbone.View
   * @screenshot views/maps/viewfinder/ViewfinderView.png
   * @since 2.28.0
   * @constructs ViewfinderView
   */
  const ViewfinderView = Backbone.View.extend(
    /** @lends ViewfinderView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "ViewfinderView",

      /**
       * The HTML class to use for this view's outermost element.
       * @type {string}
       */
      className: BASE_CLASS,

      /**
       * Values meant to be used by the rendered HTML template.
       */
      templateVars: {
        classNames: CLASS_NAMES,
      },

      /**
       * Initialize the ViewfinderView.
       * @param {object} options - The options for the view.
       * @param {Map} options.model - The map model to use for this view.
       */
      initialize({ model: mapModel }) {
        this.viewfinderModel = new ViewfinderModel({ mapModel });
        this.panelsModel = new ExpansionPanelsModel({ isMulti: true });
        this.viewfinderCardsListViews = [];

        // When the visualization overlay closes, reset active button states
        // on all preset cards so none appears stuck in an active state.
        this.listenTo(
          mapModel,
          "change:activeVisualizationUrl",
          (model, url) => {
            if (!url) {
              this.viewfinderCardsListViews.forEach((listView) => {
                listView.children?.forEach((child) => child.resetActiveState());
              });
            }
          },
        );
      },

      /**
       * Get the ViewfinderCardsView element. Renamed from getZoomPresets
       * when zoom presets were deprecated in favor of viewfinder cards in 2.37.0.
       * @returns {JQuery} The ViewfinderCardsView element.
       * @since 2.29.0
       */
      getViewfinderCards() {
        return this.$el.find(`.${CLASS_NAMES.viewfinderCardsView}`);
      },

      /**
       * Get the ViewfinderCardsView panel for a given category, if it exists.
       * @param {ViewfinderCardCategory} category The category of viewfinder
       * cards to get the panel for.
       * @returns {JQuery} The ViewfinderCardsView panel element, or an empty
       * jQuery object if it doesn't exist.
       * @since 2.35.0
       */
      getViewfinderCardsPanel(category) {
        return this.$el.find(`#${category.cid}`);
      },

      /**
       * Determine where to place a ViewfinderCardsView for a given category,
       * based on the order of categories in the collection. Renamed from
       * getZoomPresetsPlacement when zoom presets were deprecated in favor of
       * viewfinder cards in 2.37.0.
       * @param {ViewfinderCardCategory} category The category of viewfinder
       * cards to determine placement for.
       * @returns {string|object} "prepend" to add to the beginning of the list,
       * "append" to add to the end of the list, or { after: JQueryElement } to
       * add after a specific existing element.
       * @since 2.35.0
       */
      getViewfinderCardsPlacement(category) {
        const categories = this.viewfinderModel.get("viewfinderCards");
        const index = categories.indexOf(category);
        if (index === 0) return "prepend";
        const previousCategory = categories.at(index - 1);
        const previousPanel = this.getViewfinderCardsPanel(previousCategory);
        if (previousPanel?.length) return { after: previousPanel };
        return "append";
      },

      /**
       * Remove the ViewfinderCardsView panel for a given category, if it
       * exists.
       * @param {ViewfinderCardCategory} category The category of viewfinder
       * cards to remove the panel for.
       * @since 2.35.0
       */
      removeViewfinderCardsCategory(category) {
        const panel = this.getViewfinderCardsPanel(category);
        if (panel?.length) panel.remove();
      },

      /**
       * Get the SearchView element.
       * @returns {JQuery} The SearchView element.
       */
      getSearch() {
        return this.$el.find(`.${CLASS_NAMES.searchView}`);
      },

      /**
       * Helper function to focus input on the search query input and ensure
       * that the cursor is at the end of the text (as opposed to the beginning
       * which appears to be the default jQuery behavior).
       * @since 2.29.0
       */
      focusInput() {
        this.searchView.focusInput();
      },

      /**
       * Render child ViewfinderCardsListView and append to DOM. Extended from
       * renderZoomPresetsView when zoom presets were deprecated in favor of
       * generalized viewfinder cards in 2.37.0. Adds piping for visualization
       * panel as well.
       * @param {ViewfinderCardCategory} category The category of viewfinder
       * cards to render.
       * @since 2.29.0
       */
      renderViewfinderCardsView(category) {
        const viewfinderCards = category.get("viewfinderCards");
        if (!viewfinderCards.length && viewfinderCards.url) {
          viewfinderCards.fetch({
            success: () => this.renderViewfinderCardsView(category),
            error: () => this.removeViewfinderCardsCategory(category),
          });
          return;
        }

        const viewfinderCardsListView = new ViewfinderCardsListView({
          viewfinderCards,
          selectViewfinderCard: (card, action) => {
            this.viewfinderModel.selectViewfinderCard(card, action);
          },
          openVisualization: (url) => {
            this.viewfinderModel.openVisualization(url);
          },
          closeVisualization: () => {
            this.viewfinderModel.closeVisualization();
          },
          onActivate: (activeView) => {
            this.viewfinderCardsListViews.forEach((lv) => {
              lv.children?.forEach((child) => {
                if (child !== activeView) child.resetActiveState();
              });
            });
          },
        });
        viewfinderCardsListView.categoryCid = category.cid;
        this.viewfinderCardsListViews = this.viewfinderCardsListViews.filter(
          (lv) => lv.categoryCid !== category.cid,
        );
        this.viewfinderCardsListViews.push(viewfinderCardsListView);
        const expansionPanel = new ExpansionPanelView({
          contentViewInstance: viewfinderCardsListView,
          icon: category.get("icon"),
          panelsModel: this.panelsModel,
          title: category.get("label"),
          startOpen: category.get("expanded") === true,
          id: category.cid,
          variants: ["title"],
          isSvgIcon: category.get("isSvgIcon") === true,
        });
        expansionPanel.render();

        const existingPanel = this.getViewfinderCardsPanel(category);
        if (existingPanel?.length) {
          existingPanel.replaceWith(expansionPanel.el);
          return;
        }
        // otherwise, add it where it belongs according to collection order
        const placement = this.getViewfinderCardsPlacement(category);

        if (placement === "prepend") {
          this.getViewfinderCards().prepend(expansionPanel.el);
        } else if (placement === "append") {
          this.getViewfinderCards().append(expansionPanel.el);
        } else if (placement.after) {
          placement.after.after(expansionPanel.el);
        }
      },

      /** Render child SearchView and append to DOM. */
      renderSearchView() {
        this.searchView = new SearchView({
          viewfinderModel: this.viewfinderModel,
        });
        this.searchView.render();

        this.getSearch().append(this.searchView.el);
      },

      /**
       * Render the view by updating the HTML of the element.
       * The new HTML is computed from an HTML template that
       * is passed an object with relevant view state.
       */
      render() {
        this.el.innerHTML = _.template(Template)(this.templateVars);

        this.renderSearchView();

        const categories = this.viewfinderModel.get("viewfinderCards");
        categories?.each((category) =>
          this.renderViewfinderCardsView(category),
        );
      },
    },
  );

  return ViewfinderView;
});