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

define([
  "backbone",
  "jquery",
  "semantic",
  "models/metadata/eml211/EMLAttributeList",
  "views/metadata/EMLAttributeView",
  "views/metadata/AutofillAttributesView",
], (
  Backbone,
  $,
  Semantic,
  EMLAttributeList,
  EMLAttributeView,
  AutofillAttributesView,
) => {
  /**
   * @class EMLAttributesView
   * @classdesc An EMLAttributesView displays the info about all attributes in
   * an EML document or the references. It can be used to edit attributes and to
   * autofill from other entities or from a CSV file.
   * @classcategory Views/Metadata
   * @screenshot views/metadata/EMLAttributesView.png
   * @augments Backbone.View
   */

  const STRINGS = {
    addAttribute: "Add Attribute",
    newAttribute: "New Attribute",
    linkedAttributes: "Linked Attributes",
    addAttributeHelp: "Describe the first attribute in this entity",
  };

  const CLASS_NAMES = {
    menuContainer: "attribute-menu-container",
    actionButtonsContainer: "action-buttons",
    attributeList: "attribute-list",
    addAttributeHelp: "add-attribute-help",
    menu: "attribute-menu",
    menuItem: "attribute-menu-item",
    menuItemName: "name",
    ellipsis: "ellipsis",
    new: "new",
    add: "add",
    error: "error",
    remove: "remove",
    previewRemove: "remove-preview",
    autofillButton: "autofill-button",
    autofillContainer: "autofill-attributes",
    addAttributeButton: "add-attribute-button",
    referencesPanel: "references-panel",
    editReferencesButton: "edit-references-button",
    editReferencesButtonContainer: "right-aligned-flex-container",
    buttonHelpText: "button-help-text",
  };

  // Classes from bootstrap that are used in the view
  const BOOTSTRAP_CLASS_NAMES = {
    sideNavItems: "side-nav-items",
    sideNavItem: "side-nav-item",
    pointer: "pointer",
    icon: "icon",
    hidden: "hidden",
    active: "active",
    button: "btn",
    buttonPrimary: "btn-primary",
    info: "alert-info",
    badge: "label",
    well: "well",
  };

  // Fontawesome icon names used in the view
  const ICONS = {
    edit: "pencil",
    warning: "warning-sign",
    remove: "remove",
    add: "plus",
    magic: "magic",
    error: "exclamation-sign",
    onLeft: "on-left",
    processing: "time",
    success: "ok",
    link: "link",
    info: "info-sign",
  };

  // Prefix to add ato all ICONS
  const ICON_PREFIX = "icon-";

  const EMLAttributesView = Backbone.View.extend(
    /** @lends EMLAttributesView.prototype */ {
      /**
       * The className to add to the view container
       * @type {string}
       */
      className: "attribute-container",

      /**
       * Settings for the semantic UI popup used to indicate that an attribute
       * will be removed when the remove button is clicked
       * @type {object}
       */
      removeTooltipSettings: {
        content: "Remove",
        position: "top center",
        variation: `${Semantic.CLASS_NAMES.variations.inverted} ${Semantic.CLASS_NAMES.variations.mini}`,
        delay: {
          show: 400,
          hide: 20,
        },
        exclusive: true,
      },

      /**
       * The events this view will listen to and the associated function to
       * call.
       * @type {object}
       */
      events() {
        const e = {};
        const CN = CLASS_NAMES;
        e[`mouseover .${CN.menuItem} .${CN.remove}`] = "previewAttrRemove";
        e[`mouseout .${CN.menuItem} .${CN.remove}`] = "previewAttrRemove";
        e[`click .${CN.menuItem}`] = "handleMenuItemClick";
        e[`click .${CN.autofillButton}`] = "showAutofill";
        e[`click .${CN.editReferencesButton}`] = "switchToSourceEntity";
        e[`click .${CN.addAttributeHelp} a`] = "addNewAttribute";
        return e;
      },

      /**
       * Generates the HTML template for the EMLAttributesView.
       * @returns {DocumentFragment} The HTML template content for the
       * EMLAttributesView.
       */
      template() {
        const template = document.createElement("template");
        const CN = CLASS_NAMES;
        const BC = BOOTSTRAP_CLASS_NAMES;
        template.innerHTML = `
          <div class="${CN.menuContainer}">
            <div class="${CN.actionButtonsContainer}">
              <a href="#" class="${BC.button} ${BC.buttonPrimary} ${CN.autofillButton}"><i class="${ICONS.magic} ${ICONS.onLeft}"></i>Auto-Fill...</a>
            </div>
            <ul class="${CN.menu} ${BC.sideNavItems}"></ul>
          </div>
          <div class="${CN.attributeList}"></div>
          <div class="${CN.autofillContainer}" style="display: none;"></div>`;
        return template.content;
      },

      /**
       * The HTML template for an attribute
       * @type {Function}
       * @param {object} attrs - The attributes to render in the template
       * @param {string} attrs.classes - Extra classes to add to the menu item
       * separated by spaces
       * @param {string} attrs.attrId - The if of the attribute
       * @param {string} attrs.attributeName - The name of the attribute
       * @returns {HTMLElement} The HTML template for an attribute
       */
      menuItemTemplate(attrs) {
        const CN = CLASS_NAMES;
        const BC = BOOTSTRAP_CLASS_NAMES;
        const template = document.createElement("template");
        const extraClasses = attrs.classes ? ` ${attrs.classes}` : "";
        template.innerHTML = `
          <li class="${CN.menuItem} ${BC.sideNavItem} pointer ${extraClasses}" data-id="${attrs.attrId}">
            <a class="${CN.ellipsis}">
              <span class="name">${attrs.attributeName}</span>
              <i class="${BC.icon} ${ICONS.remove} ${CN.remove}"></i>
            </a>
          </li>`;
        return template.content.querySelector("li");
      },

      /**
       * Creates a template for the references panel
       * @param {string} sourceTitle - The title of the source entity
       * @param {string[]} sourceAttrNames - The names of the attributes in the
       * source entity
       * @returns {HTMLElement} The references panel element
       */
      referencesTemplate(sourceTitle, sourceAttrNames) {
        const attrList = sourceAttrNames
          .map((name) => {
            const span = document.createElement("span");
            span.classList.add(BOOTSTRAP_CLASS_NAMES.badge, "label-info");
            span.textContent = name;
            return span.outerHTML;
          })
          .join(", ");
        const CN = CLASS_NAMES;
        const BC = BOOTSTRAP_CLASS_NAMES;
        const template = document.createElement("template");
        template.innerHTML = `
          <div class="${CN.referencesPanel}">
            <h3>
              <i class="${BC.icon} ${ICONS.link} ${ICONS.onLeft}"></i>${STRINGS.linkedAttributes}
            </h3>
            <div class="${BC.well} ${CN.references}">
              <p>
                This attribute list is a <b>linked copy</b> of defined in the
                <a class="label label-info">${sourceTitle}</a> entity. 
                Any updates made to the source entity will automatically be reflected here.
              </p>
              <p>The attributes in that entity are: <b>${attrList}</b>.</p>
              <p>To make changes, please edit the source entity directly.</p>
            </div>
            <div class="${CN.editReferencesButtonContainer}">
                <a class="${BC.button} ${BC.buttonPrimary} ${CN.editReferencesButton}">
                  <i class="${BC.icon} ${ICONS.edit} ${ICONS.onLeft}"></i> Edit Attributes in Source Entity
                </a>
                <div class="${CN.buttonHelpText}">You will be re-directed to '${sourceTitle}'</div>
            </div>
          </div>`;

        return template.content.querySelector("div");
      },

      /**
       * Creates a new EMLAttributesView
       * @param {object} options - A literal object with options to pass to the
       * view
       * @param {EMLAttributes} options.collection - The collection of
       * EMLAttribute models to display
       * @param {References} [options.references] - The reference element to
       * display instead of the collection. Will be ignored if collection is
       * provided.
       * @param {EMLEntity} options.parentModel - The entity model to which
       * these attributes belong
       * @param {boolean} [options.isNew] - Set to true if this is a new
       * attribute
       */
      initialize(options = {}) {
        this.model = options.model || new EMLAttributeList();
        this.parentModel = options.parentModel;
        this.parentView = options.parentView;

        // Prefix all the icons
        if (!Object.values(ICONS)[0].startsWith(ICON_PREFIX)) {
          Object.keys(ICONS).forEach((key) => {
            ICONS[key] = `${ICON_PREFIX}${ICONS[key]}`;
          });
        }
      },

      /**
       * Renders this view
       * @returns {EMLAttributesView} A reference to this view
       */
      render() {
        // Render the template
        this.el.innerHTML = "";
        this.el.append(this.template());

        // Select the items we will update
        const CN = CLASS_NAMES;
        this.els = {
          menu: this.el.querySelector(`.${CN.menu}`),
          list: this.el.querySelector(`.${CN.attributeList}`),
          autofill: this.el.querySelector(`.${CN.autofillContainer}`),
          autofillButton: this.el.querySelector(`.${CN.autofillButton}`),
        };

        this.renderReferencesOrAttributes();
        this.renderAutofill();

        return this;
      },

      /**
       * Check if the attrList has references or attributes (since both are not
       * allowed), and render the appropriate UI.
       */
      renderReferencesOrAttributes() {
        if (this.model.hasReferences()) {
          this.references = this.model.get("references");
          this.renderReferences();
        } else {
          this.collection = this.model.get("emlAttributes");
          this.renderAttributes();
        }
      },

      /**
       * Renders the references button and panel
       * @returns {HTMLElement|null} The references button element or null if
       * the references' linked model can't be found
       */
      renderReferences() {
        const BC = BOOTSTRAP_CLASS_NAMES;
        const {
          list,
          menu,
          addAttributeButton,
          linkedAttrMenuItem,
          referencesPanel,
        } = this.els;

        // In case of a re-render
        const elsToRemove = [
          addAttributeButton,
          linkedAttrMenuItem,
          referencesPanel,
        ];

        elsToRemove.forEach((el) => el?.remove());

        const item = this.menuItemTemplate({
          attrId: "references",
          attributeName: `<i class="${BC.icon} ${ICONS.link} ${ICONS.onLeft}"></i>${STRINGS.linkedAttributes}`,
        });
        this.els.linkedAttrMenuItem = item;
        const removeIcon = item.querySelector(`.${ICONS.remove}`);
        removeIcon.remove();
        menu.appendChild(item);
        this.els.referencesButton = item;

        const sourceAttrs = this.references.getLinkedModel();

        if (!sourceAttrs) return null; // TODO: display error?

        const sourceEntity = sourceAttrs?.get("parentModel");
        const sourceTitle = sourceEntity?.get("entityName");
        const sourceAttrNames = sourceAttrs
          .get("emlAttributes")
          .pluck("attributeName");

        // TODO: Add tooltips w/ description of the attributes. Make clickable.

        const refEl = this.referencesTemplate(sourceTitle, sourceAttrNames);
        this.els.referencesPanel = refEl;
        list.append(refEl);

        const editButton = refEl.querySelector(
          `.${CLASS_NAMES.editReferencesButton}`,
        );
        // TODO: add an unlink button.

        this.els.editReferencesButton = editButton;
        this.els.referencesButton = item;

        this.els.referencesPanel = refEl;
        this.showReferences(); // activate the button

        // Keep the displayed attribute names & entity name in sync with the
        // source entity
        this.stopListening(sourceEntity, "change:entityName");
        this.listenTo(sourceEntity, "change:entityName", this.renderReferences);
        this.stopListening(sourceAttrs, "change:emlAttributes");
        this.listenTo(
          sourceAttrs,
          "change:emlAttributes",
          this.renderReferences,
        );

        return refEl;
      },

      /**
       * Close the modal containing this view and open the modal for a different
       * entity in the same entities collection. Requires that the parent
       * EMLEntityView is on this view.
       */
      switchToSourceEntity() {
        const sourceEntity = this.references
          .getLinkedModel()
          ?.get("parentModel");
        if (sourceEntity) {
          this.parentView?.switchToOtherEntityView(sourceEntity);
        }
      },

      /** Show the references panel and hide everything else */
      showReferences() {
        this.hideEverything();
        const { referencesPanel, referencesButton } = this.els;
        if (!referencesPanel || !referencesButton) return;
        referencesPanel.classList.remove(BOOTSTRAP_CLASS_NAMES.hidden);
        referencesButton.classList.add(BOOTSTRAP_CLASS_NAMES.active);
        referencesPanel.scrollIntoView();
      },

      /** Hide the references panel */
      hideReferences() {
        const { referencesPanel, referencesButton } = this.els;
        if (referencesPanel)
          referencesPanel.classList.add(BOOTSTRAP_CLASS_NAMES.hidden);
        if (referencesButton)
          referencesButton.classList.remove(BOOTSTRAP_CLASS_NAMES.active);
      },

      /**
       * Create an attribute view & menu item for each attribute in the
       * collection, set up event listeners on the attributes, and set the first
       * attribute as active. Creates a new attribute for the user to fill out.
       * This function can be used to re-render the attributes.
       */
      renderAttributes() {
        if (!this.collection) return;
        // Reset elements and event listeners in case of a re-render
        this.stopListeningToAttributesCollection();
        this.attrEls = {};
        this.els.list.innerHTML = "";
        this.els.menu.innerHTML = "";
        this.collection.each((attr) => {
          attr.set("isNew", false, { silent: true });
          // Render Attribute views for each model
          this.renderAttribute(attr);
        });

        this.renderAddAttributeMessage();

        // Show the first view, the others will be hidden
        if (this.collection.length > 0) {
          this.showAttribute(this.collection.at(0));
        } else {
          this.showAddAttributeMessage();
        }

        this.listenToAttributesCollection();

        // Render the new attribute button here because it is part of the menu.
        // The button is a list item that always remains at the bottom of the menu.
        if (this.els.addAttributeButton) {
          this.els.addAttributeButton.remove();
          this.els.addAttributeButton = null;
        }
        const button = this.createNewAttributeButton([
          CLASS_NAMES.addAttributeButton,
          CLASS_NAMES.new,
        ]);
        this.els.menu.appendChild(button);
        this.els.addAttributeButton = button;
      },

      /**
       * When there are no attrbutes yet, show a message to the user to help
       * them add an attribute. Include a button to add a new attribute.
       */
      renderAddAttributeMessage() {
        if (this.els.addAttributePanel) {
          this.els.addAttributePanel.remove();
          this.els.addAttributePanel = null;
        }
        const CN = CLASS_NAMES;
        const BC = BOOTSTRAP_CLASS_NAMES;
        const message = document.createElement("div");
        message.classList.add(BC.well, CN.addAttributeHelp);

        message.innerHTML = `
          <p><i class="${BC.icon} ${ICONS.info} ${ICONS.onLeft}"></i>${STRINGS.addAttributeHelp}</p>
          <a class="${BC.button} ${BC.buttonPrimary}"><i class="${ICONS.add} ${ICONS.onLeft}"></i>${STRINGS.addAttribute}</a>
        `;
        const button = message.querySelector("a");
        message.appendChild(button);
        this.els.list.appendChild(message);
        this.els.addAttributePanel = message;
      },

      /** Show the add attribute help message */
      showAddAttributeMessage() {
        if (this.els.addAttributePanel) {
          this.els.addAttributePanel.style.display = "block";
        } else {
          this.renderAddAttributeMessage();
        }
      },

      /** Hide the add attribute help message */
      hideAddAttributeMessage() {
        if (this.els.addAttributePanel) {
          this.els.addAttributePanel.style.display = "none";
        }
      },

      /**
       * Creates an instance of an add attribute button.
       * @param {string[]} classes - Extra classes to add to the button
       * @returns {HTMLElement} The add attribute button element
       */
      createNewAttributeButton(classes) {
        const CN = CLASS_NAMES;
        const BC = BOOTSTRAP_CLASS_NAMES;
        const classesStr = classes?.length ? classes.join(" ") : "";
        const button = this.menuItemTemplate({
          attrId: "add-attribute-button",
          attributeName: STRINGS.addAttribute,
          classes: classesStr,
        });
        // Add an add icon to the button Prepend it within the <a> tag
        const iconHtml = document.createElement("i");
        iconHtml.classList.add(BC.icon, ICONS.onLeft, ICONS.add, CN.add);
        const buttonLink = button.querySelector(`.${CN.ellipsis}`);
        buttonLink.prepend(iconHtml);

        // Find the remove icon and remove it
        const removeIcon = button.querySelector(`.${CN.remove}`);
        removeIcon?.remove();
        return button;
      },

      /**
       * Adds a blank attribute to the end of the collection for the user to
       * fill out.
       */
      addNewAttribute() {
        this.listenToOnce(this.collection, "add", (model) => {
          const { menuItem } = this.attrEls[model.cid];
          this.updateMenuItemLabel(menuItem, STRINGS.newAttribute);
          this.showAttribute(model);
        });
        this.collection.addNewAttribute(this.parentModel, true);
      },

      /** Set up event listeners for the collection and its models */
      listenToAttributesCollection() {
        if (this.collection) {
          this.listenTo(this.collection, "add", this.renderAttribute);
          this.listenTo(this.collection, "remove", this.removeAttribute);
          this.listenTo(this.collection, "sort", this.orderAttributeMenu);
          this.listenTo(this.collection, "namesUpdated", this.renderAttributes);
        }
      },

      /** Stop listening to the attributes collection */
      stopListeningToAttributesCollection() {
        if (this.collection) {
          this.stopListening(this.collection);
        }
        this.stopListening(this.model.get("emlAttributes"));
      },

      /**
       * Sets up listeners for the specified attribute model to handle various
       * events. Updates the UI and triggers appropriate actions when the
       * model's state changes.
       * @param {Backbone.Model} attributeModel - The attribute model to listen
       * to.
       */
      listenToAttributeModel(attributeModel) {
        const { menuItem } = this.attrEls[attributeModel.cid];
        this.listenTo(attributeModel, "invalid", this.showAttributeValidation);
        this.listenTo(attributeModel, "valid", this.hideAttributeValidation);
        this.listenTo(
          attributeModel,
          "change:attributeName",
          (_model, value) => {
            this.updateMenuItemLabel(menuItem, value);
          },
        );
      },

      /**
       * Stops listening to events from the specified attribute model.
       * @param {Backbone.Model} attributeModel - The attribute model to stop
       * listening to.
       */
      stopListeningToAttributeModel(attributeModel) {
        this.stopListening(attributeModel);
      },

      /**
       * Initializes event listeners for all attribute models in the collection
       * and the attributes collection itself.
       */
      startAllListeners() {
        this.collection?.models.forEach((attr) => {
          this.listenToAttributeModel(attr);
        });
        this.listenToAttributesCollection();
        this.listenTo(
          this.model,
          "change:references",
          this.renderReferencesOrAttributes,
        );
      },

      /**
       * Stops all event listeners associated with the attribute models in the
       * collection and the attributes collection itself.
       */
      stopAllListeners() {
        this.collection?.models.forEach((attr) => {
          this.stopListeningToAttributeModel(attr);
        });
        this.stopListeningToAttributesCollection();
        this.stopListening(this.model, "change:references");
      },

      /**
       * Render an attribute
       * @param {EMLAttribute} attributeModel - The attribute model to render
       */
      renderAttribute(attributeModel) {
        if (!attributeModel) return;

        // Don't render the same attribute twice
        if (this.attrEls?.[attributeModel.cid]) return;
        const { menu, list } = this.els;

        const listItem = new EMLAttributeView({
          model: attributeModel,
          parentModel: this.parentModel,
        }).render();
        list.append(listItem.el);

        const menuItem = this.createMenuItem(attributeModel);
        menu.append(menuItem);

        // Track the list and menu item elements
        if (!this.attrEls) this.attrEls = {};
        this.attrEls[attributeModel.cid] = {
          listItem,
          menuItem,
        };

        // Always scroll to the bottom of the menu so that the add attribute
        // button is always visible
        menuItem.scrollIntoView();

        // Indicate in menu item if there's a validation error; keep name of
        // attribute in sync with the model
        this.stopListeningToAttributeModel(attributeModel);
        this.listenToAttributeModel(attributeModel);

        // Hide the attribute when to start
        this.hideAttribute(attributeModel);
      },

      /**
       * Render the autofill view and save a reference to it
       * @returns {AutofillAttributesView} The autofill view
       */
      renderAutofill() {
        const autofillContainer = this.els.autofill;
        autofillContainer.innerHTML = "<div></div>";
        const el = autofillContainer.querySelector("div");
        this.autoFill = new AutofillAttributesView({
          el,
          model: this.model,
          parentModel: this.parentModel,
        }).render();
        return this.autoFill;
      },

      /**
       * Create a menu item for an attribute
       * @param {EMLAttribute} attributeModel - The attribute model
       * @returns {HTMLElement} The menu item element
       */
      createMenuItem(attributeModel) {
        if (!attributeModel) return null;
        const item = this.menuItemTemplate({
          attrId: attributeModel.cid,
          attributeName: attributeModel.get("attributeName") || "",
        });
        const removeIcon = item.querySelector(`.${ICONS.remove}`);
        $(removeIcon).popup(this.removeTooltipSettings);
        return item;
      },

      /**
       * Handle the click event on a menu item
       * @param {Event} e - The click event
       */
      handleMenuItemClick(e) {
        // Check if the target is the remove icon, if so, remove the attribute
        if (e.target.classList.contains(ICONS.remove)) {
          this.handleRemove(e);
          return;
        }
        // Find the panel associated with the clicked menu item and show it
        const menuItem = e.currentTarget;
        const attrId = menuItem.getAttribute("data-id");
        this.hideEverything();

        // The target would be the icon, link, etc. The item clicked would be
        // the parent li.
        const parentLi = e.target.closest(`.${CLASS_NAMES.menuItem}`);
        // If the target is the references menu item, show the references
        if (parentLi === this.els.referencesButton) {
          this.showReferences();
          return;
        }
        // If the clicked item is the add attribute button, add a new attribute
        if (parentLi === this.els.addAttributeButton) {
          this.addNewAttribute();
          return;
        }

        // Otherwise it's an attribute menu item, so show the attribute
        this.showAttribute(this.collection.get(attrId));
      },

      /**
       * Display the autofill view, hide the attributes.
       */
      showAutofill() {
        this.hideEverything();
        this.renderAutofill();
        this.validatePreviousAttribute();
        // Hide the attribute list
        this.els.list.style.display = "none";
        // Show the autofill view
        this.els.autofill.style.display = "block";
        // Show auto-fill button as active
        this.els.autofillButton.classList.add(BOOTSTRAP_CLASS_NAMES.active);
      },

      /**
       * Hide the autofill view and show the attributes
       */
      hideAutofill() {
        this.removeAutofill();
        // Show the list again
        this.els.list.style.display = "block";
        // Hide the autofill view
        this.els.autofill.style.display = "none";
        // Remove active status from the autofill button
        this.els.autofillButton.classList.remove(BOOTSTRAP_CLASS_NAMES.active);
      },

      /** Remove the autofill view */
      removeAutofill() {
        if (this.autoFill) {
          this.autoFill.onClose();
          this.autoFill.remove();
          this.autoFill = null;
        }
      },

      /**
       * Handle the remove event for an attribute
       * @param {Event} e - The click event
       */
      handleRemove(e) {
        const menuItem = e.currentTarget;
        const attrId = menuItem.getAttribute("data-id");
        this.collection.remove(attrId);
      },

      /**
       * Remove an attribute from the view that was just removed from the
       * collection
       * @param {EMLAttribute} attributeModel - The attribute model
       */
      removeAttribute(attributeModel) {
        const { listItem, menuItem } = this.attrEls[attributeModel.cid];
        // If the item is active, show the next item
        const menuItemActive = menuItem.classList.contains(
          BOOTSTRAP_CLASS_NAMES.active,
        );
        if (menuItemActive && this.collection.length > 0) {
          this.showAttribute(this.collection.at(0));
        }
        if (this.collection.length === 0) {
          this.showAddAttributeMessage();
        }

        listItem.remove();
        menuItem.remove();
        this.stopListening(attributeModel);
        delete this.attrEls[attributeModel.cid];
      },

      /**
       * Get the label of a menu item
       * @param {HTMLElement} menuItem - The menu item element
       * @returns {string} The label of the menu item
       */
      getMenuItemLabel(menuItem) {
        return menuItem.querySelector(`.${CLASS_NAMES.menuItemName}`)
          .textContent;
      },

      /**
       * Update the label of a menu item
       * @param {HTMLElement} menuItem - The menu item element
       * @param {string} text - The new label text
       */
      updateMenuItemLabel(menuItem, text) {
        const menuEl = menuItem;
        menuEl.querySelector(`.${CLASS_NAMES.menuItemName}`).textContent = text;
      },

      /**
       * Show an attribute
       * @param {EMLAttribute} attributeModel - The attribute model
       */
      showAttribute(attributeModel) {
        this.hideEverything();
        const { listItem, menuItem } = this.attrEls[attributeModel.cid];
        listItem.show();
        menuItem.classList.add(BOOTSTRAP_CLASS_NAMES.active);
        listItem.postRender();
        listItem.el.scrollIntoView();

        if (attributeModel.get("isNew")) {
          attributeModel.set("isNew", false);
        }

        this.validatePreviousAttribute();
      },

      /** Validate the previous attribute */
      validatePreviousAttribute() {
        // If this attribute list is references instead of attributes, there's
        // nothing to validate
        if (!this.collection) return;
        // find the active attribute listItem
        const activeAttrTab = this.el.querySelector(
          `.${BOOTSTRAP_CLASS_NAMES.active}.${CLASS_NAMES.menuItem}`,
        );
        const activeID = activeAttrTab?.getAttribute("data-id");
        // get the model associated with the active attribute
        if (activeID) {
          const activeModel = this.collection.get(activeID);
          // validate the model
          activeModel?.validate();
        }
      },

      /**
       * Show the attribute validation errors in the attribute navigation menu
       * @param {EMLAttribute} attributeModel - The attribute model
       */
      showAttributeValidation(attributeModel) {
        const attrEl = this.attrEls[attributeModel.cid];
        const menuLink = attrEl?.menuItem.querySelector("a");
        if (menuLink?.classList.contains("error")) return;
        if (!attrEl) return;
        attrEl.errorIcon = document.createElement("i");
        attrEl.errorIcon.classList.add(
          BOOTSTRAP_CLASS_NAMES.icon,
          ICONS.error,
          CLASS_NAMES.error,
          ICONS.onLeft,
        );

        menuLink?.classList.add(CLASS_NAMES.error);
        menuLink.prepend(attrEl.errorIcon);
      },

      /**
       * Hide the attribute validation errors from the attribute navigation menu
       * @param {EMLAttribute} attributeModel - The attribute model
       */
      hideAttributeValidation(attributeModel) {
        if (!attributeModel) return;
        if (!this.attrEls?.[attributeModel.cid]) return;
        const attrEl = this.attrEls[attributeModel.cid];
        const menuLink = attrEl.menuItem.querySelector("a");
        menuLink.classList.remove("error");
        if (attrEl.errorIcon) {
          attrEl.errorIcon.remove();
          attrEl.errorIcon = null;
        }
      },

      /**
       * Hide an attribute
       * @param {EMLAttribute} attributeModel - The attribute model
       */
      hideAttribute(attributeModel) {
        if (!this.attrEls?.[attributeModel?.cid]) return;
        const { listItem, menuItem } = this.attrEls[attributeModel.cid];
        listItem.hide();
        menuItem.classList.remove(BOOTSTRAP_CLASS_NAMES.active);
      },

      /** Hide all attribute views */
      hideAllAttributes() {
        this.collection?.models.forEach(this.hideAttribute, this);
      },

      /** Hide the attributes, autofill view, and references panel */
      hideEverything() {
        this.hideAllAttributes();
        this.hideAutofill();
        this.hideReferences();
        this.hideAddAttributeMessage();
      },

      /**
       * Show the user what will be removed when this remove button is clicked
       * @param {Event} e - The click event
       */
      previewAttrRemove(e) {
        const removeBtn = e.target;
        removeBtn
          .closest(`.${CLASS_NAMES.menuItem}`)
          .classList.toggle(CLASS_NAMES.previewRemove);
      },

      /** Re-order the menu items to match the order of the collection */
      orderAttributeMenu() {
        const menuItems = this.els.menu.querySelectorAll(
          `.${CLASS_NAMES.menuItem}`,
        );
        const menuItemsArray = Array.from(menuItems);
        this.collection.each((model) => {
          const menuItem = menuItemsArray.find(
            (item) => item.getAttribute("data-id") === model.cid,
          );
          if (menuItem) {
            this.els.menu.appendChild(menuItem);
          }
        });
        // Finally ensure that the add attribute button is at the end
        this.els.menu.appendChild(this.els.addAttributeButton);
      },

      /** Actions to perform when the view is removed */
      onClose() {
        // Remove empty attribute models
        this.collection?.removeEmptyAttributes();
        this.stopAllListeners();
        this.removeAutofill();
        // Destroy all popups
        const popups = this.el.querySelectorAll(`.${CLASS_NAMES.menuItem}`);
        popups.forEach((popup) => {
          $(popup).popup("destroy");
        });
      },
    },
  );

  return EMLAttributesView;
});