Source: src/js/views/maps/LayerDetailsView.js

"use strict";

define([
  "jquery",
  "underscore",
  "backbone",
  "models/maps/assets/MapAsset",
  "text!templates/maps/layer-details.html",
  // Sub-Views
  "views/maps/LayerDetailView",
  "views/maps/LayerOpacityView",
  "views/maps/LayerInfoView",
  "views/maps/LayerNavigationView",
  "views/maps/LegendView",
], (
  $,
  _,
  Backbone,
  MapAsset,
  Template,
  // Sub-Views
  LayerDetailView,
  LayerOpacityView,
  LayerInfoView,
  LayerNavigationView,
  LegendView,
) => {
  /**
   * @class LayerDetailsView
   * @classdesc A panel with additional information about a Layer (a Map Asset like
   * imagery or vector data), plus some UI for updating the appearance of the Layer on
   * the map, such as the opacity.
   * @classcategory Views/Maps
   * @name LayerDetailsView
   * @augments Backbone.View
   * @screenshot views/maps/LayerDetailsView.png
   * @since 2.18.0
   * @constructs
   */
  const LayerDetailsView = Backbone.View.extend(
    /** @lends LayerDetailsView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "LayerDetailsView",

      /**
       * The HTML classes to use for this view's element
       * @type {string}
       */
      className: "layer-details",

      /**
       * The MapAsset model that this view uses
       * @type {MapAsset}
       */
      model: undefined,

      /**
       * The primary HTML template for this view
       * @type {Underscore.template}
       */
      template: _.template(Template),

      /**
       * Classes that are used to identify the HTML elements that comprise this view.
       * @type {object}
       * @property {string} open The class to add to the outermost HTML element for this
       * view when the layer details view is open/expanded (not hidden)
       * @property {string} toggle The element in the template that acts as a toggle to
       * close/hide the details view
       * @property {string} sections The container for all of the LayerDetailViews.
       * @property {string} label The label element for the layer that displays a title
       * in the header of the details view
       * @property {string} notification The element that holds the notification message,
       * if there is one. Inserted before all the details sections.
       * @property {string} badge The class to add to the badge element that is shown
       * when the layer has a notification message.
       */
      classes: {
        open: "layer-details--open",
        toggle: "layer-details__toggle",
        sections: "layer-details__sections",
        label: "layer-details__label",
        notification: "layer-details__notification",
        badge: "map-view__badge",
      },

      /**
       * Configuration for a Layer Detail section to show within this Layer Details
       * view.
       * @typedef {object} DetailSectionOption
       * @property {string} label The name to display for this section
       * @property {Backbone.View} view Any view that will render content for the Layer
       * Detail section. This view will be passed the MapAsset model. The view should
       * display information about the MapAsset and/or allow some aspect of the
       * MapAsset's appearance to be edited - e.g. a LayerInfoView or a
       * LayerOpacityView.
       * @property {boolean} collapsible Whether or not this section should be
       * expandable and collapsible.
       * @property {boolean} showTitle Whether or not to show the title/label for this
       * section.
       * @property {boolean} hideIfError Set to true to hide this section when there is
       * an error loading the layer. Example: we should hide the opacity slider for
       * layers that are not visible on the map
       */

      /**
       * A list of sections to render within this view that give details about the
       * MapAsset, or allow editing of the MapAsset appearance. Each section will have a
       * title and its content will be collapsible.
       * @type {DetailSectionOption[]}
       */
      sections: [
        {
          label: "Navigation",
          view: LayerNavigationView,
          collapsible: false,
          showTitle: false,
          hideIfError: true,
        },
        {
          label: "Legend",
          view: LegendView,
          collapsible: false,
          showTitle: true,
          hideIfError: true,
        },
        {
          label: "Opacity",
          view: LayerOpacityView,
          collapsible: false,
          showTitle: true,
          hideIfError: true,
        },
        {
          label: "Info & Data",
          view: LayerInfoView,
          collapsible: true,
          showTitle: true,
          hideIfError: false,
        },
      ],

      /**
       * Creates an object that gives the events this view will listen to and the
       * associated function to call. Each entry in the object has the format 'event
       * selector': 'function'.
       * @returns {object}
       */
      events() {
        const events = {};
        // Close the layer details panel when the toggle button is clicked. Get the
        // class of the toggle button from the classes property set in this view.
        events[`click .${this.classes.toggle}`] = "close";
        return events;
      },

      /**
       * Whether or not the layer details view is open
       * @type {boolean}
       */
      isOpen: false,

      /**
       * Executed when a new LayerDetailsView is created
       * @param {object} [options] - A literal object with options to pass to the view
       */
      initialize(options) {
        try {
          // Get all the options and apply them to this view
          if (typeof options === "object") {
            Object.keys(options).forEach((key) => {
              this[key] = options[key];
            });
          }
        } catch (e) {
          console.log(
            `A LayerDetailsView failed to initialize. Error message: ${e}`,
          );
        }
      },

      /**
       * Renders this view
       * @returns {LayerDetailsView | null} Returns the rendered view element
       */
      render() {
        try {
          const { model } = this;

          // Show the layer details box as open if the view is set to have it open
          // already
          if (this.isOpen) {
            this.el.classList.add(this.classes.open);
          }

          // Insert the template into the view
          this.$el.html(
            this.template({
              label: model ? model.get("label") || "" : "",
            }),
          );

          // Ensure the view's main element has the given class name
          this.el.classList.add(this.className);

          // Select elements in the template that we will need to manipulate
          const sectionsContainer = this.el.querySelector(
            `.${this.classes.sections}`,
          );
          const labelEl = this.el.querySelector(`.${this.classes.label}`);

          // Render each section in the Details panel
          this.renderedSections = _.clone(this.sections);

          // Remove and do not render opacity section if showOpacitySlider is false
          if (model?.get("showOpacitySlider") === false) {
            this.renderedSections = this.renderedSections.filter(
              (item) => item.label !== "Opacity",
            );
          }

          this.renderedSections.forEach((section) => {
            const detailSection = new LayerDetailView({
              label: section.label,
              contentView: section.view,
              model,
              collapsible: section.collapsible,
              showTitle: section.showTitle,
            });
            sectionsContainer.append(detailSection.el);
            detailSection.render();
            // Hide the section if there is an error with the asset, and this section
            // does make sense to show for a layer that can't be displayed
            if (section.hideIfError && model) {
              if (model && model.get("status") === "error") {
                detailSection.el.style.display = "none";
              }
            }
            section.renderedView = detailSection;
          });

          // Hide/show sections with the 'hideIfError' property when the status of the
          // MapAsset changes
          this.stopListening(model, "change:status");
          this.listenTo(model, "change:status", (_model, status) => {
            const hideIfErrorSections = _.filter(
              this.renderedSections,
              (section) => section.hideIfError,
            );
            let displayProperty = "";
            if (status === "error") {
              displayProperty = "none";
            }
            hideIfErrorSections.forEach((section) => {
              const renderedViewEl = section.renderedView.el;
              renderedViewEl.style.display = displayProperty;
            });
          });

          // If this layer has a notification, show the badge and notification
          // message
          const notice = model ? model.get("notification") : null;
          if (notice && (notice.message || notice.badge)) {
            // message
            if (notice.message) {
              const noticeEl = document.createElement("div");
              noticeEl.classList.add(this.classes.notification);
              noticeEl.innerText = notice.message;
              if (notice.style) {
                const badgeClass = `${this.classes.notification}--${notice.style}`;
                noticeEl.classList.add(badgeClass);
              }
              sectionsContainer.prepend(noticeEl);
            }
            // badge
            if (notice.badge) {
              const badge = document.createElement("span");
              badge.classList.add(this.classes.badge);
              badge.innerText = notice.badge;
              if (notice.style) {
                const badgeClass = `${this.classes.badge}--${notice.style}`;
                badge.classList.add(badgeClass);
              }
              labelEl.append(badge);
            }
          }

          return this;
        } catch (error) {
          console.log(
            `There was an error rendering a LayerDetailsView` +
              `. Error details: ${error}`,
          );
          return null;
        }
      },

      /**
       * Show/expand the Layer Details panel. Opening the panel also changes the
       * MapAsset model's 'selected attribute' to true.
       */
      open() {
        try {
          this.el.classList.add(this.classes.open);
          this.isOpen = true;
          // Ensure that the model is marked as selected
          if (this.model) {
            this.model.set("selected", true);
          }
        } catch (error) {
          console.log(
            `There was an error opening the LayerDetailsView` +
              `. Error details: ${error}`,
          );
        }
      },

      /**
       * Hide/collapse the Layer Details panel. Closing the panel also changes the
       * MapAsset model's 'selected attribute' to false.
       */
      close() {
        try {
          this.el.classList.remove(this.classes.open);
          this.isOpen = false;
          // Ensure that the model is not marked as selected
          if (this.model) {
            this.model.set("selected", false);
          }
        } catch (error) {
          console.log(
            `There was an error closing the LayerDetailsView` +
              `. Error details: ${error}`,
          );
        }
      },

      /**
       * Updates the MapAsset model set on the view then re-renders the view and
       * displays information about the new model.
       * @param {MapAsset|null} newModel the new MapAsset model to use to render the
       * view. If set to null, then the view will be rendered without any layer
       * information.
       */
      updateModel(newModel) {
        try {
          // Remove listeners from sub-views
          this.renderedSections.forEach((section) => {
            if (
              section.renderedView &&
              typeof section.renderedView.onClose === "function"
            ) {
              section.renderedView.onClose();
            }
          });
          this.model = newModel;
          this.render();
        } catch (error) {
          console.log(
            `There was an error updating the MapAsset model in a LayerDetailsView` +
              `. Error details: ${error}`,
          );
        }
      },
    },
  );

  return LayerDetailsView;
});