Source: src/js/views/filters/FilterGroupsView.js

/*global define */
define(['jquery', 'underscore', 'backbone',
        'collections/Filters',
        'models/filters/Filter',
        'models/filters/FilterGroup',
        'views/filters/FilterGroupView',
        'views/filters/FilterView',
        'text!templates/filters/filterGroups.html'],
  function($, _, Backbone, Filters, Filter, FilterGroup, FilterGroupView, FilterView, Template) {
  'use strict';

  /**
  * @class FilterGroupsView
  * @classdesc Creates a view of one or more FilterGroupViews
  * @classcategory Views/Filters
  * @name FilterGroupsView
  * @extends Backbone.View
  * @constructor
  */
  var FilterGroupsView = Backbone.View.extend(
    /** @lends FilterGroupsView.prototype */{

    /**
    * The FilterGroup models to display in this view
    * @type {FilterGroup[]}
    */
    filterGroups: [],

    /**
    * The Filters Collection that contains the same Filter
    * models from each FilterGroup and any additional Filter Models that may not be in
    * FilterGroups because they're not displayed or applied behind the scenes.
    * @type {Filters}
    */
    filters: null,
    
    /**
     * A reference to the PortalEditorView
     * @type {PortalEditorView}
    */
    editorView: undefined,

    /**
    * @inheritdoc
    */
    tagName: "div",

    /**
    * @inheritdoc
    */
    className: "filter-groups tabbable",

    /**
     * The template for this view. An HTML file is converted to an Underscore.js template
     * @since 2.17.0
     */
    template: _.template(Template),

    /**
    * If true, displays the FilterGroups in a vertical list
    * @type {Boolean}
    */
    vertical: false,

    /**
     * Set to true to render this view as a FilterGroups editor; allow the user add, edit,
     * and remove FilterGroups (TODO), and to add, delete, and edit filters within groups.
     * @type {boolean}
     * @since 2.17.0
     */
    edit: false,
    
    /**
     * If set to true, then all filters within this group will be collapsible.
     * See {@link FilterView#collapsible}
     * @type {boolean}
     * @since 2.25.0
     * @default false
     */
    collapsible: false,

    /**
     * The initial query to use when the view is first rendered. This is a text value
     * that will be set on the general `text` Solr field.
     * @type {string}
     * @since 2.25.0
     */
    initialQuery: undefined,

    /**
    * @inheritdoc
    */
    events: {
      "click .remove-filter" : "handleRemove",
      "click .clear-all"     : "removeAllFilters"
    },

    /**
    * @inheritdoc
    */
    initialize: function (options) {

      if( !options || typeof options != "object" ){
        var options = {};
      }

      this.filterGroups = options.filterGroups || new Array();
      this.filters = options.filters || new Filters();

      // For portal search filters, ID filters should be added to the query with an AND
      // operator, so that ID searches search *within* the definition collection.
      if(this.filters){
        this.filters.mustMatchIds = true
      }

      if( options.vertical == true ){
        this.vertical = true;
      }

      this.parentView = options.parentView || null;
      this.editorView = options.editorView || null;

      if(options.edit === true){
        this.edit = true
      }

      if (options.initialQuery) {
        this.initialQuery = options.initialQuery;
      }

      if (options.collapsible && typeof options.collapsible === "boolean") {
        this.collapsible = options.collapsible;
      }

    },

    /**
    * @inheritdoc
    */
    render: function () {

      //Since this view may be re-rendered at some point, empty the element and remove listeners
      this.$el.empty();
      this.stopListening();

      // Add information about editing the filter groups if this view is in edit mode
      if(this.edit){
        var title = "Change how people can search for data within your collection"
        var isNew = this.filterGroups.length === 0;
        if (this.filterGroups.length === 1 && this.filterGroups[0].isEmpty()) {
          var isNew = true;
        }

        if (isNew) {
          title = "Add filters to help people find data within your collection"
        }

        var description = "Search filters allow people to filter your data by specific " +
          "metadata fields.",
          learnMoreUrl = MetacatUI.appModel.get("portalSearchFiltersInfoURL");

        if (learnMoreUrl) {
          description = description + ' <a href="' + learnMoreUrl + '">Learn more</a>'
        }

        this.$el.html(this.template({
          title: title,
          description: description,
          helpText: ""
        }));

        // Remove this when the custom search filter builder is no longer new:
        this.$el
          .find(".port-editor-subtitle")
          .append($('<span class="new-icon" style="margin-left:10px; font-size:1rem; line-height: 25px;"><i class="icon icon-star icon-on-right"></i> NEW </span>'));
      }

        
      //Create an unordered list for all the filter tabs
      var groupTabs = $(document.createElement("ul")).addClass("nav nav-tabs filter-group-links");

      // Until we allow adding/editing filter groups in the portal data page, hide the group tabs
      // element if the portal does not already have groups in the editor.
      if (this.filterGroups.length === 1 && !this.filterGroups[0].get("label") && !this.filterGroups[0].get("icon")){
        groupTabs.hide()
      }

      //Create a container div for the filter groups
      var filterGroupContainer = $(document.createElement("div")).addClass("tab-content");

      //Add the filter group elements to this view
      this.$el.append(groupTabs, filterGroupContainer);

      var divideIntoGroups = true;

      _.each( this.filterGroups, function(filterGroup){

        //If there is only one filter group specified, and there is no label or icon,
        // then don't divide the filters into separate filter groups
        if( this.filterGroups.length == 1 && !this.filterGroups[0].get("label") &&
            !this.filterGroups[0].get("icon") ){
          divideIntoGroups = false;
        }

        if( divideIntoGroups ){
          //Create a link to the filter group
          var groupTab  = $(document.createElement("li")).addClass("filter-group-link");
          var groupLink = $(document.createElement("a"))
                              .attr("href", "#" + filterGroup.get("label").replace( /([^a-zA-Z0-9])/g, "") )
                              .attr("data-toggle", "tab");

          //Add the FilterGroup icon
          if( filterGroup.get("icon") ){
            groupLink.append( $(document.createElement("i")).addClass("icon icon-" + filterGroup.get("icon")) );
          }

          //Add the FilterGroup label
          if( filterGroup.get("label") ){
            groupLink.append(filterGroup.get("label"));
          }

          //Insert the link into the tab and add the tab to the tab list
          groupTab.append(groupLink);
          groupTabs.append(groupTab);

          //Create a tooltip for the link
          groupTab.tooltip({
            placement: "top",
            title: filterGroup.get("description"),
            trigger: "hover",
            delay: {
              show: 800
            }
          });

          //Make all the tab widths equal
          groupTab.css("width", (100 / this.filterGroups.length) + "%");
        }

        // Create a FilterGroupView. Ensure the FilterGroup is in edit mode if the parent
        // FilterGroups is.
        var filterGroupView = new FilterGroupView({
          model: filterGroup,
          edit: this.edit,
          editorView: this.editorView,
          collapsible: this.collapsible
        });

        //Render the FilterGroupView
        filterGroupView.render();

        //Add the FilterGroupView element to this view
        filterGroupContainer.append(filterGroupView.el);

        //Store a reference to the FilterGroupView in the tab link
        if( divideIntoGroups ){
          groupLink.data("view", filterGroupView);
        }

        //If a new filter is ever added to this filter group, re-render this view
        this.listenTo( filterGroup.get("filters"), "add remove", this.render );
      }, this);

      if( divideIntoGroups ){
        //Mark the first filter group as active
        groupTabs.children("li").first().addClass("active");

        //When each filter group tab is shown, perform any post render function, if needed.
        this.$('a[data-toggle="tab"]').on('shown', function (e) {
          //Get the filter group view
          var filterGroupView = $(e.target).data("view");

          //If there is a post render function, call it
          if( filterGroupView && filterGroupView.postRender ){
            filterGroupView.postRender();
          }

        });
      }

      //Mark the first filter group as active
      var firstFilterGroupEl = filterGroupContainer.find(".filter-group").first();
      firstFilterGroupEl.addClass("active");
      var activeFilterGroup = firstFilterGroupEl.data("view");

      //Call postRender() now for the active FilterGroup, since the `shown` event
      // won't trigger until/unless it's hidden then shown again.
      if( activeFilterGroup ){
        activeFilterGroup.postRender();
      }

      // Applied filters and the general search input are not needed when this view is
      // in editing mode
      if(!this.edit){
        //Add a header element above the filter groups
        this.$el.prepend( $(document.createElement("div")).addClass("filters-header") );

        //Render the applied filters
        this.renderAppliedFiltersSection();

        // Render an "All" filter. If the view was initialized with an initial
        // query, set it on this filter. 
        this.renderAllFilter(this.initialQuery);
      }

      if(this.edit){
        this.$el.addClass("edit-mode");
      }

      if( this.vertical ){
        this.$el.addClass("vertical");
      }

    },

    /**
    * Renders the section of the view that will display the currently-applied filters
    */
    renderAppliedFiltersSection: function(){

      //Add a title to the header
      var appliedFiltersContainer = $(document.createElement("div")).addClass("applied-filters-container"),
          headerText = $(document.createElement("h5"))
                        .addClass("filters-title")
                        .text("Current search")
                        .append( $(document.createElement("a"))
                                  .text("Clear all")
                                  .addClass("clear-all")
                                  .prepend( $(document.createElement("i"))
                                              .addClass("icon icon-remove icon-on-left") ));

      //Make the applied filters list
      var appliedFiltersEl = $(document.createElement("ul")).addClass("applied-filters");

      //Add the applied filters element to the filters header
      appliedFiltersContainer.append(headerText, appliedFiltersEl);
      this.$(".filters-header").append(appliedFiltersContainer);

      //Get all the nonNumeric filter models. Reject nested filterGroups.
      var nonNumericFilters = this.filters.reject(function(filterModel){
        return (["FilterGroup", "NumericFilter", "DateFilter"].includes(filterModel.type));
      });
      //Listen to changes on the "values" attribute for nonNumeric filters
      _.each(nonNumericFilters, function(nonNumericFilter){

        this.listenTo(nonNumericFilter, "change:values", this.updateAppliedFilters);

        if( nonNumericFilter.get("values").length ){
          this.updateAppliedFilters(nonNumericFilter, { displayWithoutChanges: true });
        }
      }, this);

      //Get the numeric filters and listen to the min and max values
      var numericFilters = _.where(this.filters.models, { type: "NumericFilter" });
      _.each(numericFilters, function(numericFilter){

        if( numericFilter.get("range") == true ){
          this.listenTo(numericFilter, "change:min change:max", this.updateAppliedRangeFilters);

          var filterDefaults = numericFilter.defaults();

          if( numericFilter.get("min") != filterDefaults.min ||
              numericFilter.get("max") != filterDefaults.max ||
              numericFilter.get("values").length ){
            this.updateAppliedRangeFilters(numericFilter, { displayWithoutChanges: true });
          }
        }
        else{
          this.listenTo(numericFilter, "change:values", this.updateAppliedRangeFilters);

          if( numericFilter.get("values")[0] != numericFilter.defaults().values[0] ){
            this.updateAppliedRangeFilters(numericFilter, { displayWithoutChanges: true });
          }
        }

      }, this);

      //Get the date filters and listen to the min and max values
      var dateFilters = _.where(this.filters.models, { type: "DateFilter" });
      _.each(dateFilters, function(dateFilter){
        this.listenTo(dateFilter, "change:min change:max", this.updateAppliedRangeFilters);

        if( dateFilter.get("min") != dateFilter.defaults().min ||
            dateFilter.get("max") != dateFilter.defaults().max ){
          this.updateAppliedRangeFilters(dateFilter, { displayWithoutChanges: true });
        }
      }, this);

      //When a Filter has been removed from the Filters collection, remove it's DOM element from the page
      this.listenTo(this.filters, "remove", function(removedFilter){
        this.removeAppliedFilterElByModel(removedFilter);
      });

    },

    /**
     * Renders an "All" filter that will search the general `text` Solr field
     * @param {string} searchFor - The initial value of the "All" filter. This
     * will get set on the filter model and trigger a change event. Optional.
     */
    renderAllFilter: function (searchFor="") {

      //Create an "All" filter that will search the general `text` Solr field
      var filter = new Filter({
        fields: ["text"],
        label: "Search",
        description: "Search the datasets by typing in any keyword, topic, creator, etc.",
        placeholder: "Search these datasets"
      });
      this.filters.add( filter );

      //Create a FilterView for the All filter
      var filterView = new FilterView({
        model: filter
      });
      this.listenTo(filter, "change:values", this.updateAppliedFilters);

      //Render the view and add the element to the filters header
      filterView.render();
      this.$(".filters-header").prepend(filterView.el);

      if (searchFor && searchFor.length) {
        filter.set('values', [searchFor]);
      }
    },

    postRender: function(){

      var groupTabs = this.$(".filter-group-links");

      //Check if there is a difference in heights
      var maxHeight = 0;

      _.each( groupTabs.find("a"), function(link){

        if( $(link).height() > maxHeight ){
          maxHeight = $(link).height();
        }

      });

      //Set the height of each filter group link so they are all equal
      _.each( groupTabs.find("a"), function(link){

        if( $(link).height() < maxHeight ){
          $(link).height(maxHeight + "px");
        }

      });
    },

    /**
    * Renders the values of the given Filter Model in the current filter model
    *
    * @param {Filter} filterModel - The FilterModel to display
    * @param {object} options - Additional options for this function
    * @property {boolean} options.displayWithoutChanges - If true, this filter will
    * display even if the value hasn't been changed
    */
    updateAppliedFilters: function(filterModel, options){

      //Create an options object if one wasn't sent
      if( typeof options != "object" ){
        var options = {};
      }
      this.options = options;
      var view = this;

      //If the value of this filter has changed, or if the displayWithoutChanges option
      // was passed, and if the filter is not invisible, then display it
      if( !filterModel.get("isInvisible") &&
          ((filterModel.changed && filterModel.changed.values) ||
          options.displayWithoutChanges) ){

        //Get the new values and the previous values
        var newValues      = options.displayWithoutChanges? filterModel.get("values") : filterModel.changed.values,
            previousValues = options.displayWithoutChanges? [] : filterModel.previousAttributes().values,
            //Find the values that were removed
            removedValues  = _.difference(previousValues, newValues),
            //Find the values that were added
            addedValues    = _.difference(newValues, previousValues);

        //If a filter has been added, display it
        _.each(addedValues, function(value){
          //Add the applied filter to the view
          this.$(".applied-filters").append( this.createAppliedFilter(filterModel, value) );

        }, this);

        //Iterate over each removed filter value and remove them
        _.each(removedValues, function(value){
          //Find all applied filter elements with a matching value
          var matchingFilters = this.$(".applied-filter[data-value='" + value + "']");

          //Iterate over each filter element with a matching value
          _.each(matchingFilters, function(matchingFilter){

            //If this is the filter element associated with this filter model, then remove it
            if( $(matchingFilter).data("model") == filterModel ){
              $(matchingFilter).remove();
            }

          });

        }, this);

      }

      //Toggle the applied filters header
      this.toggleAppliedFiltersHeader();

    },

    /**
     * Hides or shows the applied filter list title/header, as well as the help
     * message that lets the user know they can add filters when there are none
     */
    toggleAppliedFiltersHeader: function(){

      //If there is an applied filter
      if( this.$(".applied-filter").length ){
        // hide the "add some filters" help text
        //$(this.parentView.helpTextContainer).css("display", "none");
        // show the Clear All button
        this.$(".filters-title").css("display", "block");
      }
      //If there are no applied filters
      else{
        // show the "add some filters" help text
      //  $(this.parentView.helpTextContainer).css("display", "block");
        // hide the Clear All button
        this.$(".filters-title").css("display", "none");
      }

    },

    /**
    * When a NumericFilter or DateFilter model is changed, update the applied filters in the UI
    * @param {DateFilter|NumericFilter} filterModel - The model whose values to display
    * @param {object} [options] - Additional options for this function
    * @property {boolean} [options.displayWithoutChanges] - If true, this filter will display even if the value hasn't been changed
    */
    updateAppliedRangeFilters: function(filterModel, options){

      if( !filterModel ){
        return;
      }

      if( typeof options === "undefined" || !options ){
        var options = {};
      }

      //If the Filter is invisible, don't render it
      if( filterModel.get("isInvisible") ){
        return;
      }

      //If the minimum and maximum values are set to the default, remove the filter element
      if( filterModel.get("min") == filterModel.get("rangeMin") &&
          filterModel.get("max") == filterModel.get("rangeMax")){

        //Find the applied filter element for this filter model
        _.each(this.$(".applied-filter"), function(filterEl){

          if( $(filterEl).data("model") == filterModel ){
            //Remove the applied filter element
            $(filterEl).remove();
          }

        }, this);

      }
      //If the values attribue has changed, or if the displayWithoutChanges attribute was passed
      else if( (filterModel.changed && (filterModel.changed.min || filterModel.changed.max)) ||
                options.displayWithoutChanges ){

        //Create the filter label for ranges of numbers
        var filterValue = filterModel.getReadableValue();

        //Create the applied filter
        var appliedFilter = this.createAppliedFilter(filterModel, filterValue);

        //Keep track if this filter is already displayed and needs to be replaced
        var replaced = false;

        //Check if this filter model already has an applied filter in the UI
        _.each(this.$(".applied-filter"), function(appliedFilterEl){

          //If this applied filter already is displayed, replace it
          if( $(appliedFilterEl).data("model") == filterModel ){
            //Replace the applied filter element with the new one
            $(appliedFilterEl).replaceWith(appliedFilter);
            replaced = true;
          }

        }, this);

        if( !replaced ){
          //Add the applied filter to the view
          this.$(".applied-filters").append(appliedFilter);
        }

      }

      this.toggleAppliedFiltersHeader();

    },

    /**
    * Creates a single applied filter element and returns it. Filters can
    *  have multiple values, so one value is passed to this function at a time.
    * @param {Filter} filterModel - The Filter model that is being added to the display
    * @param {string|number|Boolean} value - The new value set on the Filter model that is displayed in this applied filter
    * @returns {jQuery} - The complete applied filter element
    */
    createAppliedFilter: function(filterModel, value){

      //Create the filter label
      var filterLabel = filterModel.get("label"),
          filterValue = value;

      //If the filter type is Choice, get the choice label which can be different from the value
      if( filterModel.type == "ChoiceFilter" ){
        //Find the choice object with the given value
        var matchingChoice = _.findWhere(filterModel.get("choices"), { "value" : value });

        //Get the label for that choice
        if(matchingChoice){
          filterValue = matchingChoice.label;
        }
      }
      //Create the filter label for boolean filters
      else if( filterModel.type == "BooleanFilter" ){

        //If the filter is set to false, remove the applied filter element
        if( filterModel.get("values")[0] === false ){

          //Iterate over the applied filters
          _.each(this.$(".applied-filter"), function(appliedFilterEl){

            //If this is the applied filter element for this model,
            if( $(appliedFilterEl).data("model") == filterModel ){
              //Remove the applied filter element from the page
              $(appliedFilterEl).remove();
            }

          }, this);

          //Exit the function at this point since there is nothing else to
          // do for false BooleanFilters
          return;
        }
        else if( filterModel.get("values")[0] === true ){
          if( !filterLabel ){
            filterLabel = filterModel.get("fields")[0];
            filterValue = "";
          }
        }

      }
      else if( filterModel.type == "ToggleFilter" ){

        if( filterModel.get("values")[0] == filterModel.get("trueValue") ){
          if( filterModel.get("label") && filterModel.get("trueLabel") ){
            filterValue = filterModel.get("trueLabel");
          }
          else if( !filterModel.get("label") && filterModel.get("trueLabel") ){
            filterLabel = "";
            filterValue = filterModel.get("trueLabel");
          }
          else if( filterModel.get("label") ){
            filterLabel = "";
            filterValue = filterModel.get("label");
          }
        }
        else{
          if( filterModel.get("label") && filterModel.get("falseLabel") ){
            filterValue = filterModel.get("falseLabel");
          }
          else if( !filterModel.get("label") && filterModel.get("falseLabel") ){
            filterLabel = "";
            filterValue = filterModel.get("falseLabel");
          }
          else if( filterModel.get("label") ){
            filterLabel = "";
            filterValue = filterModel.get("label");
          }
        }

      }
      //If this Filter model is a full-text search, don't display a label
      else if( filterModel.get("fields").length == 1 && filterModel.get("fields")[0] == "text"){
        filterLabel = "";
      }
      //isPartOf filters should just display the label, not the value
      else if( filterModel.get("fields").length == 1 && filterModel.get("fields")[0] == "isPartOf" ){
        filterValue = "";
      }
      //If the filter value is just an asterisk (i.e. `match anything`), just display the label
      else if( filterModel.get("values").length == 1 && filterModel.get("values")[0] == "*" ){
        filterValue = "";
      }
      //Filters with the valueLabels attribute want to display an alternate value from the raw value here
      else if ( filterModel.get("valueLabels") ) {
        filterValue = filterModel.get("valueLabels")[value] || value;
      }
      else if( !filterLabel ){
        filterLabel = filterModel.get("fields")[0];
      }

      //Create the applied filter element
      var removeIcon    = $(document.createElement("a"))
                            .addClass("icon icon-remove remove-filter icon-on-right")
                            .attr("title", "Remove this filter"),
          appliedFilter = $(document.createElement("li"))
                            .addClass("applied-filter label")
                            .append(removeIcon)
                            .data("model", filterModel)
                            .attr("data-value", value);

      //Create an element to contain both the label and value
      var filterLabelEl = $(document.createElement("span")).addClass("label");
      var filterValueEl = $(document.createElement("span")).addClass("value").text(filterValue);

      var filterTextContainer = $(document.createElement("span"))
                                 .append(filterLabelEl, filterValueEl);

      //If there is both a label and value, separated them with a colon
      if( filterLabel && filterValue ){
        filterLabelEl.text( filterLabel + ": ");
      }
      //Otherwise just use the label text only
      else if( filterLabel ){
        filterLabelEl.text(filterLabel);
      }

      //Add the filter text to the filter element
      appliedFilter.prepend(filterTextContainer);

      // Add a tooltip to the filter
      if(filterModel.get("description")){

        appliedFilter.tooltip({
          placement: "right",
          title: filterModel.get("description"),
          trigger: "hover",
          delay: {
            show: 700
          }
        });

      }

      return appliedFilter;
    },


    /**
    * Adds a custom filter that likely exists outside of the FilterGroups but needs
    * to be displayed with these other applied fitlers.
    *
    * @param {Filter} filterModel - The Filter Model to display
    */
    addCustomAppliedFilter: function(filterModel){

      //If the Filter is invisible, don't render it
      if( filterModel.get("isInvisible") ){
        return;
      }

      //If this filter already exists in the applied filter list, exit this function
      var alreadyExists = _.find( this.$(".applied-filter.custom"), function(appliedFilterEl){
        return $(appliedFilterEl).data("model") == filterModel;
      });

      if( alreadyExists ){
        return;
      }

      //Create the applied filter element
      var removeIcon    = $(document.createElement("a"))
                            .addClass("icon icon-remove remove-filter icon-on-right")
                            .attr("title", "Remove this filter"),
          filterText    = $(document.createElement("span")).text(filterModel.get("label")),
          appliedFilter = $(document.createElement("li"))
                            .addClass("applied-filter label custom")
                            .append(filterText, removeIcon)
                            .data("model", filterModel)
                            .attr("data-value", filterModel.get("values"));

      if( filterModel.type == "SpatialFilter" ){
        filterText.prepend( $(document.createElement("i"))
                              .addClass("icon icon-on-left icon-" + filterModel.get("icon")) );

      }

      //Add the applied filter to the view
      this.$(".applied-filters").append(appliedFilter);

      //Display the filters title
      this.toggleAppliedFiltersHeader();

    },

    /**
    * Removes the custom applied filter from the UI.
    *
    * @param {Filter} filterModel - The Filter Model to display
    */
    removeCustomAppliedFilter: function(filterModel){

      _.each(this.$(".custom.applied-filter"), function(appliedFilterEl){
        if( $(appliedFilterEl).data("model") == filterModel ){
          $(appliedFilterEl).remove();
          this.trigger("customAppliedFilterRemoved", filterModel);
        }
      }, this);

      //Hide the filters title
      this.toggleAppliedFiltersHeader();

    },

    /**
    * When a remove button is clicked, get the filter model associated with it
    /* and remove the filter from the filter group
    *
    * @param {Event} - The DOM Event that occured on the filter remove icon
    */
    handleRemove: function(e){

      // Ensure tooltips are removed
      try{
        if(e.delegateTarget){
          $(e.delegateTarget).find(".tooltip").remove();
        }
      }
      catch(e) {
        console.log("Could not remove tooltip from filter label, error message: " + e);
      };

      //Get the applied filter element and the filter model associated with it
      var appliedFilterEl = $(e.target).parents(".applied-filter"),
          filterModel =  appliedFilterEl.data("model");

      if( appliedFilterEl.is(".custom") ){
        this.removeCustomAppliedFilter(filterModel);
      }
      else{
        //Remove the filter from the filter group model
        this.removeFilter(filterModel, appliedFilterEl);
      }

    },

    /**
    * Remove the filter from the UI and the Search collection
    * @param {Filter} filterModel The Filter to remove from the Filters collection
    * @param {Element} appliedFilterEl The DOM Element for the applied filter on the page
    * @param {object} options Additional options for this function
    * @param {boolean} options.removeSilently If true, the Filter model will be removed siltently from the Filters collection.
    *  This is useful when removing multiple Filters at once, and triggering a remove/change/reset event after all have
    *  been removed.
    */
    removeFilter: function(filterModel, appliedFilterEl, options){

      var removeSilently = false;

      //Create an options object if one wasn't sent
      if( typeof options != "object" ){
        var options = {};
      }
      this.options = options;
      var view = this;

      //Parse all the additional options for this function
      if( typeof options == "object" ){
        removeSilently = typeof options.removeSilently != "undefined"? options.removeSilently : false;
      }


      if( filterModel ){
        //NumericFilters and DateFilters get the min and max values reset
        if( filterModel.type == "NumericFilter" || filterModel.type == "DateFilter" ){

          //Set the min and max values
          filterModel.set({
            min: filterModel.get("rangeMin"),
            max: filterModel.get("rangeMax"),
            values: filterModel.defaults().values
          });

          if( !removeSilently ){
            //Trigger the reset event
            filterModel.trigger("rangeReset");
          }

        }
        //For all other filter types
        else{
          //Get the current value
          var modelValues = filterModel.get("values"),
              thisValue   = $(appliedFilterEl).data("value");

          //Numbers that are set on the element `data` are stored as type `number`, but when `number`s are
          // set on Backbone models, they are converted to `string`s. So we need to check for this use case.
          if( typeof thisValue == "number" ){
            //Convert the number to a string
            thisValue = thisValue.toString();
          }

          //Remove the value that was in this applied filter
          var newValues = _.without(modelValues, thisValue),
              setOptions = {};

          if( removeSilently ){
            setOptions.silent = true;
          }

          //Updates the values on the model
          filterModel.set("values", newValues, setOptions);
        }

      }

    },

    /**
    * Gets all the applied filters in this view and their associated filter models
    *   and removes them.
    */
    removeAllFilters: function(){

      let removedFilters = [];

      //Iterate over each applied filter in the view
      _.each( this.$(".applied-filter"), function(appliedFilterEl){

        var $appliedFilterEl = $(appliedFilterEl);

        removedFilters.push($appliedFilterEl.data("model"));

        if( $appliedFilterEl.is(".custom") ){
          this.removeCustomAppliedFilter( $appliedFilterEl.data("model") );
        }
        else{

          //Remove the filter from the fitler group. Do this silently since we will trigger a "reset" event later
          this.removeFilter( $appliedFilterEl.data("model"), appliedFilterEl, { removeSilently: true } );

        }

        //Remove the applied filter element from the page
        $appliedFilterEl.remove();

      }, this);

      //Trigger the reset event on the Filters collection
      this.filters.trigger("reset");

      //Trigger the remove event on all the models now that they are all removed
      _.invoke(removedFilters, "trigger", "remove");

      //Toggle the applied filters header
      this.toggleAppliedFiltersHeader();

    },

    /**
    * Remove the applied filter element for the given model
    * This only removed the element from the page, it doesn't update the model at all or
    * trigger any events.
    * @param {Filter} - The Filter model whose elements will be deleted
    */
    removeAppliedFilterElByModel: function(filterModel){

      //Iterate over each applied filter element and find the matching filters
      this.$(".applied-filter").each(function(i, el){
        if( $(el).data("model") == filterModel ){
          //Remove the element from the page
          $(el).remove();
        }
      });

      //Toggle the applied filters header
      this.toggleAppliedFiltersHeader();

    }

  });
  return FilterGroupsView;
});