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

"use strict";

define([
  "jquery",
  "underscore",
  "backbone",
  "text!templates/maps/vector-filter-panel.html",
  "models/maps/assets/MapAsset",
  "views/searchSelect/SearchSelectView",
  "models/maps/assets/Cesium3DTileset",
], ($, _, Backbone, Template, MapAsset, SearchSelect) => {
  /**
   * @class VectorFilterView
   * @classdesc VectorFilterView is a shared component for visualizing data
   * based on the "filter" attributes set up in the map configuration file. The
   * initial implementation of this component focuses on time-series data
   * visualization.
   * @classcategory Views/Maps
   * @name VectorFilterView
   * @augments Backbone.View
   * @since 2.34.0
   * @constructs VectorFilterView
   */

  const VectorFilterView = Backbone.View.extend(
    /** @lends VectorFilterView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "VectorFilterView",

      /**
       * The 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}
       */
      classes: {
        attributeSelectClass: "layer-details--open",
        selected: "layer-item--selected",
        filterPropertyDropdownContainer:
          "filter-by-attribute__attributes-container",
        valuesDropdownContainer: "filter-by-attribute__values-container",
      },

      /**
       * Executed when a new VectorFilterView is created
       * @param {object} attributes - The attributes to initialize the view with
       */
      initialize(attributes) {
        // Allow the view to be initialized with a model
        if (attributes.model) {
          this.model = attributes.model; // Set the model from the passed attributes
        }
        this.filters = this.model?.get("filters"); // Retrieve filters attribute from the Map model
        this.filterModel = this.filters?.at(0); // Get the first filter model
      },

      /**
       * Renders this view
       * @returns {VectorFilterView} Returns the rendered view element
       */
      render() {
        // Exit early if there's no filter model
        if (!this.filterModel) {
          this.$el.html(); // Clear DOM elements if filter model does not exist
          return this;
        }

        // Insert the template into the view
        this.$el.html(this.template({}));

        // Add filter properties dropdown (to select the attribute/property to
        // filter on)
        this.renderPropertySelect();

        // Add filter property values dropdown (to select specific values for
        // the chosen attribute/property)
        this.renderValueSelect();

        return this;
      },

      /**
       * Renders the dropdown for selecting a property (i.e., filterable
       * attributes) from the filter models. These filterable attributes are
       * defined by the "property" key in the portal configuration (Map model).
       */
      renderPropertySelect() {
        const filterPropertyOptions = [];

        // Collect all filter model properties (i.e., attributes that are
        // "filterable") for the selected layer
        this.filters.each((filterModel) => {
          const propertyName = filterModel.get("property");
          filterPropertyOptions.push({
            label: propertyName,
            value: propertyName,
          });
        });

        const filterProperty = this.filterModel.get("property");

        // Initialize the property selection dropdown using SearchSelect
        this.propretySelect = new SearchSelect({
          options: filterPropertyOptions,
          allowMulti: false, // Allow only a single attribute to be selected at a time
          inputLabel: "Select property",
          placeholderText: "Property name",
          clearable: false,
          //  TODO: after a filter model is created is should not be part of
          //  subsequent filter models This needs to be part of a parent
          //  VectorFiltersView
          selected: filterProperty ? [filterProperty] : [],
          // disabled: true, // Can be enabled later when multiple properties
          // are supported
        });

        // Append the dropdown to the DOM and render it
        const propretySelectContainer = this.$(
          `.${this.classes.filterPropertyDropdownContainer}`,
        );
        propretySelectContainer.append(this.propretySelect.el);
        this.propretySelect.render();

        // Listen for changes to the selected property and update the value
        // selector accordingly
        this.stopListening(this.propretySelect.model, "change:selected");
        this.listenTo(
          this.propretySelect.model,
          "change:selected",
          (_model, propretySelection) => {
            this.handlePropertyChange(propretySelection);
          },
        );
      },

      /**
       * Updates the property values dropdown for the selected filter property
       * @param {string[]} selectedFilterProperty - The property selected in the
       * filter property dropdown in an array format.
       */
      handlePropertyChange(selectedFilterProperty) {
        const selectedProperty = selectedFilterProperty?.[0];

        this.filterModel.set("property", selectedProperty);
        this.filterModel.set("values", []); // Clear values when property changes

        let allValues = [];
        let selectedValues = [];
        this.filters.each((filterModel) => {
          const property = filterModel.get("property");
          if (selectedProperty === property) {
            allValues = filterModel.get("allValues") || [];
            selectedValues = filterModel.get("values") || [];
          }
        });

        // Update the filter model with the new values
        this.filterModel.set("allValues", allValues);
        this.filterModel.set("values", selectedValues);

        // Re-render
        this.render();
      },

      /**
       * Renders the dropdown for selecting values corresponding to a filter
       * property selection. Retrieves values from the (vector filter) model.
       * During initialization, the filterStatus is false, and the "values"
       */
      renderValueSelect() {
        const propertyAllValuesOptions = [];

        const propertyAllValues = this.filterModel.get("allValues") || [];
        if (Array.isArray(propertyAllValues)) {
          propertyAllValues.forEach((val) => {
            propertyAllValuesOptions.push({
              label: val,
              value: val,
            });
          });
        }
        const selectedValues = [...(this.filterModel.get("values") || [])];

        const valuesSelectContainer = this.$(
          `.${this.classes.valuesDropdownContainer}`,
        )[0];

        if (!this.valuesSelect) {
          const filterValuesSelect = new SearchSelect({
            options: propertyAllValuesOptions,
            placeholderText: "Values",
            inputLabel: "Select value(s)",
            allowMulti: true,
            allowAdditions: false,
            separatorTextOptions: false,
            selected: selectedValues,
          });

          valuesSelectContainer.appendChild(filterValuesSelect.el); // Append to DOM
          filterValuesSelect.render();

          this.valuesSelect = filterValuesSelect;

          // Listen for changes Call the function to update layer visibility
          // based on filter values set on the filter model
          this.stopListening(this.valuesSelect.model, "change:selected");
          this.listenTo(
            this.valuesSelect.model,
            "change:selected",
            (_model, valuesSelected) => {
              this.handleValueChange(valuesSelected);
            },
          );
        } else {
          // Update dropdown values later when the visibility of the layer is
          // toggled on and off. The dropdown elements already exist at this
          // stage.
          this.valuesSelect.updateOptions(propertyAllValuesOptions);
          this.valuesSelect.model.setSelected(selectedValues);

          if (!valuesSelectContainer.contains(this.valuesSelect.el)) {
            valuesSelectContainer.appendChild(this.valuesSelect.el);
          }
          this.valuesSelect.render();
        }
      },

      /**
       * Update the filter values in the filter model based on selection made in
       * the Filter by Property dropdowns Update the map model. Update layer
       * visibility and icons.
       * @param {string[]} selectedValues - The values that are selected in
       * values the Select View
       */
      handleValueChange(selectedValues) {
        const filterValues = (selectedValues || []).filter(
          (value) => value !== "",
        ); // filterValues will be 0 when everything is cleared

        this.filterModel.set("values", [...filterValues]);

        // If there is any value selected in the Filter by Property feature,
        // then make sure the asset is also visible. This updates the layer
        // toggle visibility icon (i.e., eye icon) and makes the layer visible
        // on the map.
        if (filterValues?.length && !this.model.isVisible()) {
          this.model.show();
        } else if (!filterValues?.length && this.model.isVisible()) {
          // When all values are cleared from the attribute values dropdown, the
          // layer visibility is set to false, and the filter icon is turned off
          // (i.e., transparent).
          this.model.set("visible", false);
        }
      },
    },
  );

  return VectorFilterView;
});