Source: src/js/views/metadata/EMLTaxonView.js

define([
  "jquery",
  "underscore",
  "backbone",
  "views/searchSelect/SearchSelectView",
  "models/metadata/eml211/EMLTaxonCoverage",
  "text!templates/metadata/taxonomicCoverage.html",
  "text!templates/metadata/taxonomicClassificationTable.html",
  "text!templates/metadata/taxonomicClassificationRow.html",
], (
  $,
  _,
  Backbone,
  SearchSelect,
  EMLTaxonCoverage,
  TaxonomicCoverageTemplate,
  TaxonomicClassificationTableTemplate,
  TaxonomicClassificationRowTemplate,
) => {
  /**
   * @class EMLTaxonView
   * @classdesc A Backbone View that renders the taxonomic coverage section of
   * an EML model. This view is used to create, edit, and delete taxonomic
   * classifications in the EML model. It also provides a "quick add" interface
   * for adding common taxa to the taxonomic coverage section. Logic oiginally
   * included in the EML211EditorView.
   * @augments Backbone.View
   * @since 2.33.0
   */
  const EMLTaxonView = Backbone.View.extend(
    /** @lends EMLTaxonView.prototype */ {
      /** @inheritdoc */
      events: {
        "change .taxonomic-coverage": "updateTaxonCoverage",
        "keyup .taxonomic-coverage .new input": "addNewTaxon",
        "keyup .taxonomic-coverage .new select": "addNewTaxon",
        "focusout .taxonomic-coverage tr": "showTaxonValidation",
        "click .taxonomic-coverage-row .remove": "removeTaxonRank",
        "mouseover .taxonomic-coverage .remove": "previewTaxonRemove",
        "mouseout .taxonomic-coverage .remove": "previewTaxonRemove",
      },

      /**
       * The template for the taxonomic coverage section
       * @type {UnderscoreTemplate}
       */
      taxonomicCoverageTemplate: _.template(TaxonomicCoverageTemplate),

      /**
       * The template for the taxonomic classification table
       * @type {UnderscoreTemplate}
       */
      taxonomicClassificationTableTemplate: _.template(
        TaxonomicClassificationTableTemplate,
      ),

      /**
       * The template for the taxonomic classification row
       * @type {UnderscoreTemplate}
       */
      taxonomicClassificationRowTemplate: _.template(
        TaxonomicClassificationRowTemplate,
      ),

      /** @inheritdoc */
      initialize(options = {}) {
        this.parentModel = options.parentModel;
        this.taxonArray = this.getTaxonArray(options);
        this.edit = options.edit || false;

        // If duplicates are removed while saving, make sure to re-render the
        // taxa
        const view = this;
        this.taxonArray.forEach((taxonCov) => {
          this.stopListening(taxonCov);
          this.listenTo(
            taxonCov,
            "duplicateClassificationsRemoved",
            this.render,
          );
        }, view);
      },

      /**
       * Get the taxon coverage array from the options or the parent model
       * @param {object} options - The options passed to the view
       * @returns {EMLTaxonCoverage[]} An array of EMLTaxonCoverage models
       */
      getTaxonArray(options = {}) {
        const taxonArray = options?.taxonArray;

        // Don't need  to make a new taxon array if we already have one
        if (
          taxonArray?.length &&
          Array.isArray(taxonArray) &&
          taxonArray[0] instanceof EMLTaxonCoverage
        ) {
          return taxonArray;
        }

        // Try getting the taxon coverage from the parent model or adding one to
        // the parent model
        if (options.parentModel) {
          return options.parentModel.hasTaxonomicCoverage()
            ? options.parentModel.get("taxonCoverage")
            : options.parentModel.addTaxonomicCoverage(true);
        }

        // If we have no parent EML model, then just create a new taxon coverage
        return [new EMLTaxonCoverage()];
      },

      /** @inheritdoc */
      render() {
        const view = this;
        const { el } = this;
        const { $el } = this;

        el.innerHTML = `<h2>Taxa</h2>`;

        this.taxonArray.forEach((coverage) => {
          $el.append(this.createTaxonomicCoverage(coverage));
        });

        this.updateTaxaNumbering();

        // Insert the quick-add taxon options, if any are configured for this
        // theme. See {@link AppModel#quickAddTaxa}
        view.renderTaxaQuickAdd();

        return this;
      },

      /**
       * Update the numbering of the taxa in the taxonomic coverage section
       * @returns {HTMLElement[]} An array of the taxon numbering elements
       * @since 2.33.0
       */
      updateTaxaNumbering() {
        const taxaNums = this.el.querySelectorAll(".editor-header-index");
        taxaNums.forEach((taxaNum, i) => {
          const element = taxaNum;
          element.textContent = i + 1;
        });

        return taxaNums;
      },

      /**
       * Creates a table to hold a single EMLTaxonCoverage element (table) for
       * each root-level taxonomicClassification
       * @param {EMLTaxonCoverage} coverage - An EMLTaxonCoverage model
       * @returns {jQuery} A jQuery object containing the HTML for the taxonomic
       * coverage section
       */
      createTaxonomicCoverage(coverage) {
        const finishedEls = $(
          this.taxonomicCoverageTemplate({
            generalTaxonomicCoverage:
              coverage.get("generalTaxonomicCoverage") || "",
          }),
        );
        const coverageEl = finishedEls.filter(".taxonomic-coverage");

        coverageEl.data({ model: coverage });

        const classifications = coverage.get("taxonomicClassification");

        // Makes a table... for the root level
        classifications.forEach((classification) => {
          coverageEl.append(
            this.createTaxonomicClassificationTable(classification),
          );
        }, this);

        // Create a new, blank table for another taxonomicClassification
        const newTableEl = this.createTaxonomicClassificationTable();

        coverageEl.append(newTableEl);

        return finishedEls;
      },

      /**
       * Create the HTML for a taxonomic coverage table
       * @param {EMLTaxonCoverage} classification - An EMLTaxonCoverage model
       * @returns {jQuery} A jQuery object containing the HTML for a taxonomic
       * coverage table
       */
      createTaxonomicClassificationTable(classification) {
        const taxaNums = this.updateTaxaNumbering();

        // Adding the taxoSpeciesCounter to the table header for enhancement of the view
        const finishedEl = $(
          '<div class="row-striped root-taxonomic-classification-container"></div>',
        );
        $(finishedEl).append(
          `<h6>Species <span class="editor-header-index">${
            taxaNums.length + 1
          }</span> </h6>`,
        );

        // Add a remove button if this is not a new table
        if (!(typeof classification === "undefined")) {
          $(finishedEl).append(
            this.createRemoveButton(
              "taxonCoverage",
              "taxonomicClassification",
              ".root-taxonomic-classification-container",
              ".taxonomic-coverage",
            ),
          );
        }

        const tableEl = $(this.taxonomicClassificationTableTemplate());
        const tableBodyEl = $(document.createElement("tbody"));

        const queue = [classification];
        const rows = [];
        let cur;

        while (queue.length > 0) {
          cur = queue.pop();

          // I threw this in here so I can this function without an
          // argument to generate a new table from scratch
          if (cur) {
            cur.taxonRankName = cur.taxonRankName?.toLowerCase();
            rows.push(cur);
            if (cur.taxonomicClassification) {
              Object.entries(cur.taxonomicClassification).forEach(
                ([_taxonRank, taxonClass]) => {
                  queue.push(taxonClass);
                },
              );
            }
          }
        }

        rows.forEach((row) => {
          tableBodyEl.append(this.makeTaxonomicClassificationRow(row));
        });

        const newRowEl = this.makeNewTaxonomicClassificationRow();

        $(tableBodyEl).append(newRowEl);
        $(tableEl).append(tableBodyEl);

        // Add the new class to the entire table if it's a new one
        if (typeof classification === "undefined") {
          $(tableEl).addClass("new");
        }

        $(finishedEl).append(tableEl);

        return finishedEl;
      },

      /**
       * @external TaxonomicClassification
       * @see EMLTaxonCoverage.TaxonomicClassification
       */

      /**
       * Create the HTML for a single row in a taxonomicClassification table
       * @param {TaxonomicClassification} classification A classification object
       * from an EMLTaxonCoverage model, may include a taxonRank, taxonValue,
       * taxonId, commonName, and nested taxonomicClassification objects
       * @returns {jQuery} A jQuery object containing the HTML for a single row
       * in a taxonomicClassification table
       * @since 2.24.0
       */
      makeTaxonomicClassificationRow(classification = {}) {
        const finishedEl = $(
          this.taxonomicClassificationRowTemplate({
            taxonRankName: classification.taxonRankName || "",
            taxonRankValue: classification.taxonRankValue || "",
          }),
        );
        // Save a reference to other taxon attributes that we need to keep
        // when serializing the model
        if (classification.taxonId) {
          $(finishedEl).data("taxonId", classification.taxonId);
        }
        if (classification.commonName) {
          $(finishedEl).data("commonName", classification.commonName);
        }
        return finishedEl;
      },

      /**
       * Create the HTML for a new row in a taxonomicClassification table
       * @returns {jQuery} A jQuery object containing the HTML for a new row
       * in a taxonomicClassification table
       * @since 2.24.0
       */
      makeNewTaxonomicClassificationRow() {
        const row = this.makeTaxonomicClassificationRow({});
        $(row).addClass("new");
        return row;
      },

      /**
       * Update the underlying model and DOM for an EML TaxonomicCoverage
       * section. This method handles updating the underlying TaxonomicCoverage
       * models when the user changes form fields as well as inserting new
       * form fields automatically when the user needs them.
       *
       * Since a dataset has multiple TaxonomicCoverage elements at the dataset
       * level, each Taxonomic Coverage is represented by a table element and
       * all taxonomicClassifications within are rows in that table.
       *
       * TODO: Finish this function
       * TODO: Link this function into the DOM
       * @param {object} options - An object with the following properties:
       * - target: The target element that was changed
       * - coverage: The taxonomic coverage element that was changed
       */
      updateTaxonCoverage(options) {
        let coverage;
        let model;
        let value;
        let e;
        let classificationEl;

        if (options.target) {
          // Ignore the event if the target is a quick add taxon UI element.
          const quickAddEl = $(this.taxonQuickAddEl);
          if (quickAddEl && quickAddEl.has(options.target).length) {
            return;
          }

          e = options;

          // Getting `model` here is different than in other places because the
          // thing being updated is an `input` or `select` element which is part
          // of a `taxonomicClassification`. The model is `TaxonCoverage` which
          // has one or more `taxonomicClassifications`. So we have to walk up
          // to the hierarchy from input < td < tr < tbody < table < div to get
          // at the underlying TaxonCoverage model.

          coverage = $(e.target).parents(".taxonomic-coverage");
          classificationEl = $(e.target).parents(
            ".root-taxonomic-classification",
          );
          model = $(coverage).data("model") || this.parentModel;
          const category = $(e.target).attr("data-category");
          value = this.parentModel?.cleanXMLText($(e.target).val());

          // We can't update anything without a coverage, or
          // classification
          if (!coverage) return;
          if (!classificationEl) return;

          // Use `category` to determine if we're updating the generalTaxonomicCoverage or
          // the taxonomicClassification
          if (category && category === "generalTaxonomicCoverage") {
            model.set("generalTaxonomicCoverage", value);

            return;
          }
        } else {
          coverage = options.coverage;
          model = $(coverage).data("model");
        }

        // Find all of the root-level taxonomicClassifications
        const classificationTables = $(coverage).find(
          ".root-taxonomic-classification",
        );

        if (!classificationTables) return;

        // TODO: This should be refactored into tidy functions.

        let rows;
        const collectedClassifications = [];

        Array.from(classificationTables).forEach((classificationTable) => {
          rows = $(classificationTable).find("tbody tr");

          if (!rows) return;

          const topLevelClassification = {};
          let classification = topLevelClassification;
          let currentRank;
          let currentValue;

          Array.from(rows).forEach((row, j) => {
            const thisRow = row;

            currentRank =
              this.parentModel?.cleanXMLText($(thisRow).find("select").val()) ||
              "";
            currentValue =
              this.parentModel?.cleanXMLText($(thisRow).find("input").val()) ||
              "";

            // Maintain classification attributes that exist in the EML but are not visible in the editor
            const taxonId = $(thisRow).data("taxonId");
            const commonName = $(thisRow).data("commonName");

            // Skip over rows with empty Rank or Value
            if (!currentRank.length || !currentValue.length) {
              return;
            }

            // After the first row, start nesting taxonomicClassification objects
            if (j > 0) {
              classification.taxonomicClassification = [{}];
              [classification] = classification.taxonomicClassification;
            }

            // Add it to the classification object
            classification.taxonRankName = currentRank;
            classification.taxonRankValue = currentValue;
            classification.taxonId = taxonId;
            classification.commonName = commonName;
          });

          // Add the top level classification to the array
          if (Object.keys(topLevelClassification).length) {
            collectedClassifications.push(topLevelClassification);
          }
        });

        if (
          !_.isEqual(
            collectedClassifications,
            model.get("taxonomicClassification"),
          )
        ) {
          model.set("taxonomicClassification", collectedClassifications);
          this.parentModel?.trigger("change");
        }

        // Handle adding new tables and rows
        // Do nothing if the value isn't set
        if (value) {
          // Add a new row if this is itself a new row
          if ($(e.target).parents("tr").first().is(".new")) {
            const newRowEl = this.makeNewTaxonomicClassificationRow();
            $(e.target).parents("tbody").first().append(newRowEl);
            $(e.target).parents("tr").first().removeClass("new");
          }

          // Add a new classification table if this is itself a new table
          if ($(classificationEl).is(".new")) {
            $(classificationEl).removeClass("new");
            $(classificationEl).append(
              this.createRemoveButton(
                "taxonCoverage",
                "taxonomicClassification",
                ".root-taxonomic-classification-container",
                ".taxonomic-coverage",
              ),
            );
            $(coverage).append(this.createTaxonomicClassificationTable());
          }
        }

        // update the quick add interface
        this.updateQuickAddTaxa();
      },

      /**
       * Update the options for the quick add taxon select interface. This
       * ensures that only taxonomic classifications that are not already
       * included in the taxonomic coverage are available for selection.
       * @since 2.24.0
       */
      updateQuickAddTaxa() {
        const selects = this.taxonSelects;
        if (!selects || !selects.length) return;
        const taxa = this.getTaxonQuickAddOptions();
        if (!taxa || !taxa.length) return;
        selects.forEach((select, i) => {
          select.updateOptions(taxa[i].options);
        });
      },

      /**
       * Adds a new row and/or table to the taxonomic coverage section
       * @param {Event} e - The event that triggered this function
       */
      addNewTaxon(e) {
        // Don't do anything if the current classification doesn't have new content
        if ($(e.target).val().trim() === "") return;

        // If the row is new, add a new row to the table
        if ($(e.target).parents("tr").is(".new")) {
          const newRow = this.makeNewTaxonomicClassificationRow();
          // Append the new row and remove the new class from the old row
          $(e.target).parents("tr").removeClass("new").after(newRow);
        }
      },

      /**
       * Insert the "quick add" interface for adding common taxa to the
       * taxonomic coverage section. Only renders if there is a list of taxa
       * configured in the appModel.
       */
      renderTaxaQuickAdd() {
        const view = this;
        // To render the taxon select, the view must be in editor mode and we
        // need a list of taxa configured for the theme
        if (!view.edit) return;

        // remove any existing quick add interface:
        if (view.taxonQuickAddEl) view.taxonQuickAddEl.remove();

        const quickAddTaxa = view.getTaxonQuickAddOptions();

        if (!quickAddTaxa || !quickAddTaxa.length) {
          // If the taxa are configured as SID for a dataObject, then wait
          // for the dataObject to be loaded
          this.listenToOnce(
            MetacatUI.appModel,
            "change:quickAddTaxa",
            this.renderTaxaQuickAdd,
          );
          return;
        }

        // Create & insert the basic HTML for the taxon select interface
        const template = `<div class="taxa-quick-add">
              <p class="taxa-quick-add__text">
                <b>⭐️ Quick Add Taxa:</b> Select one or more common taxa. Click "Add" to add them to the list.
              </p>
              <div class="taxa-quick-add__controls">
              <div class="taxa-quick-add__selects"></div>
              <button class="btn btn-primary taxa-quick-add__button">Add Taxa</button>
              </div>
            </div>`;
        const parser = new DOMParser();
        const doc = parser.parseFromString(template, "text/html");
        const quickAddEl = doc.body.firstChild;
        const button = quickAddEl.querySelector("button");
        const container = quickAddEl.querySelector(".taxa-quick-add__selects");
        const rowSelector = ".root-taxonomic-classification-container";
        const firstRow = document.querySelector(rowSelector);
        firstRow.parentNode.insertBefore(quickAddEl, firstRow);
        view.taxonQuickAddEl = quickAddEl;

        // Update the taxon coverage when the button is clicked
        const onButtonClick = () => {
          const { taxonSelects } = view;
          if (!taxonSelects || !taxonSelects.length) return;
          const selectedItems = taxonSelects
            .map((select) => select.model.get("selected"))
            .flat();
          if (!selectedItems || !selectedItems.length) return;
          const selectedItemObjs = selectedItems.map((item) => {
            try {
              // It will be encoded JSON if it's a pre-defined taxon
              return JSON.parse(decodeURIComponent(item));
            } catch (e) {
              // Otherwise it will be a string a user typed in
              return {
                taxonRankName: "",
                taxonRankValue: item,
              };
            }
          });
          view.addTaxa(selectedItemObjs);
          taxonSelects.forEach((select) =>
            select.model.setSelected([], { silent: true }),
          );
        };
        button.removeEventListener("click", onButtonClick);
        button.addEventListener("click", onButtonClick);

        // Create the search selects
        view.taxonSelects = [];
        quickAddTaxa.forEach((taxaList) => {
          try {
            const taxaInput = new SearchSelect({
              options: taxaList.options,
              placeholderText: taxaList.placeholder,
              inputLabel: taxaList.label,
              allowMulti: true,
              allowAdditions: true,
              separatorTextOptions: false,
              selected: [],
            });
            container.appendChild(taxaInput.el);
            taxaInput.render();
            view.taxonSelects.push(taxaInput);
          } catch (e) {
            // Skip this taxa list if it can't be rendered
            view.taxonSelects.push(`Error: ${e}`);
          }
        });
      },

      /**
       * Get the list of options for the taxon quick add interface. Filter
       * out any that have already been added to the taxonomic coverage.
       * @returns {object[]} An array of search select options
       * @since 2.24.0
       */
      getTaxonQuickAddOptions() {
        const quickAddTaxa = MetacatUI.appModel.getQuickAddTaxa();
        if (!quickAddTaxa || !quickAddTaxa.length) return null;

        const coverages = this.parentModel?.get("taxonCoverage");
        const quickAddTaxaOpts = quickAddTaxa.map((taxaList) => {
          const opts = [];
          taxaList.taxa.forEach((taxon) => {
            // check that it is not a duplicate in any coverages
            const isDuplicate = coverages.some((cov) => cov.isDuplicate(taxon));
            if (!isDuplicate) {
              opts.push(this.taxonOptionToSearchSelectItem(taxon));
            }
          });
          // clone so we don't modify the original
          const newTaxaList = { ...taxaList };
          newTaxaList.options = opts;
          return newTaxaList;
        });
        return quickAddTaxaOpts;
      },

      /**
       * Reformats a taxon option, as provided in the appModel
       * {@link AppModel#quickAddTaxa}, as a search select item.
       * @param {object} option A single taxon classification with at least a
       * taxonRankValue and taxonRankName. It may also have a taxonId (object
       * with provider and value) and a commonName.
       * @returns {object} A search select item with label, value, and
       * description properties.
       */
      taxonOptionToSearchSelectItem(option) {
        // option must have a taxonRankValue and taxonRankName or it is invalid
        if (!option.taxonRankValue || !option.taxonRankName) {
          return null;
        }
        // Create a description
        let description = `${option.taxonRankName}: ${option.taxonRankValue}`;
        if (option.taxonId) {
          description += ` (${option.taxonId.provider}: ${option.taxonId.value})`;
        }
        // search select doesn't work with some of the json characters
        const val = encodeURIComponent(JSON.stringify(option));
        return {
          label: option.commonName || option.taxonRankValue,
          value: val,
          description,
        };
      },

      /**
       * Add new taxa to the EML model and re-render the taxa section. The new
       * taxa will be added to the first <taxonomicCoverage> element in the EML
       * model. If there is no <taxonomicCoverage> element, one will be created.
       * @param {object[]} newClassifications - An array of objects with any of
       * the following properties:
       *  - taxonRankName: (sting) The name of the taxonomic rank, e.g.
       *    "Kingdom"
       *  - taxonRankValue: (string) The value of the taxonomic rank, e.g.
       *    "Animalia"
       *  - commonName: (string) The common name of the taxon, e.g. "Animals"
       *  - taxonId: (object) The official ID of the taxon, including "provider"
       *    and "value".
       *  - taxonomicClassification: (array) An array of nested taxonomic
       *    classifications
       * @since 2.24.0
       * @example
       * this.addTaxon([{
       *  taxonRankName: "Kingdom",
       *  taxonRankValue: "Animalia",
       *  commonName: "Animals",
       *  taxonId: {
       *    provider: "https://www.itis.gov/",
       *    value: "202423"
       *  }]);
       */
      addTaxa(newClassifications) {
        // TODO: validate the new taxon before adding it to the model?
        const taxonCoverages = this.parentModel?.get("taxonCoverage");
        // We expect that there is already a taxonCoverage array on the model.
        // If the EML was made in the editor, there can only be one
        // <taxonomicCoverage> element. Add the new taxon to its
        // <taxonomicClassification> array. If there is more than one, then the
        // new taxon will be added to the first <taxonomicCoverage> element.
        if (taxonCoverages && taxonCoverages.length >= 1) {
          const taxonCoverage = taxonCoverages[0];
          const classifications = taxonCoverage.get("taxonomicClassification");
          const allClass = classifications.concat(newClassifications);
          taxonCoverage.set("taxonomicClassification", allClass);
        } else {
          // If there is no <taxonomicCoverage> element for some reason,
          // create one and add the new taxon to its <taxonomicClassification>
          // array.
          const newCov = new EMLTaxonCoverage({
            taxonomicClassification: newClassifications,
            parentModel: this.parentModel,
          });
          this.parentModel?.set("taxonCoverage", [newCov]);
        }
        // Re-render the taxa section
        this.render();
      },

      /**
       * Remove a taxonomic classification from the EML model
       * @param {Event} e - The event that triggered this function
       */
      removeTaxonRank(e) {
        const row = $(e.target).parents(".taxonomic-coverage-row");
        const coverageEl = $(row).parents(".taxonomic-coverage");
        const view = this;

        // Animate the row away and then remove it
        row.slideUp("fast", () => {
          row.remove();
          view.updateTaxonCoverage({ coverage: coverageEl });
        });
      },

      /**
       * After the user focuses out, show validation help, if needed
       * @param {Event} e - The event that triggered this function
       */
      showTaxonValidation(e) {
        // Get the text inputs and select menus
        const row = $(e.target).parents("tr");
        const allInputs = row.find("input, select");
        const tableContainer = $(e.target).parents("table");
        const errorInputs = [];

        // If none of the inputs have a value and this is a new row, then do nothing
        if (_.every(allInputs, (i) => !i.value) && row.is(".new")) return;

        // Add the error styling to any input with no value
        _.each(allInputs, (input) => {
          // Keep track of the number of clicks of each input element so we only show the
          // error message after the user has focused on both input elements
          if (!input.value) errorInputs.push(input);
        });

        if (errorInputs.length) {
          // Show the error message after a brief delay
          setTimeout(() => {
            // If the user focused on another element in the same row, don't do anything
            if (_.contains(allInputs, document.activeElement)) return;

            // Add the error styling
            $(errorInputs).addClass("error");

            // Add the error message
            if (!tableContainer.prev(".notification").length) {
              tableContainer.before(
                $(document.createElement("p"))
                  .addClass("error notification")
                  .text("Enter a rank name AND value in each row."),
              );
            }
          }, 200);
        } else {
          allInputs.removeClass("error");

          if (!tableContainer.find(".error").length)
            tableContainer.prev(".notification").remove();
        }
      },

      /**
       * Indicate that a taxonomic classification is about to be removed if the
       * button is clicked
       * @param {Event} e - The event that triggered this function
       */
      previewTaxonRemove(e) {
        const removeBtn = $(e.target);

        if (removeBtn.parent().is(".root-taxonomic-classification")) {
          removeBtn.parent().toggleClass("remove-preview");
        } else {
          removeBtn
            .parents(".taxonomic-coverage-row")
            .toggleClass("remove-preview");
        }
      },

      /**
       * Creates "Remove" buttons for removing non-required sectionsof the EML
       * from the DOM
       * @param {string} submodel - The name of the submodel to remove
       * @param {string} attribute - The name of the attribute to remove
       * @param {string} selector - The selector for the element to remove
       * @param {string} container - The selector for the container element
       * @returns {jQuery} A jQuery object containing the remove button
       * // TODO: duplicate of EML211EditorView#createRemove
       */
      createRemoveButton(submodel, attribute, selector, container) {
        return $(document.createElement("span"))
          .addClass("icon icon-remove remove pointer")
          .attr("title", "Remove")
          .data({
            submodel,
            attribute,
            selector,
            container,
          });
      },
    },
  );

  return EMLTaxonView;
});