Source: src/js/views/ontologies/BioontologyBrowserView.js

define([
  "backbone",
  "semantic",
  "models/connectors/Bioontology-Accordion-SearchSelect",
  "views/accordion/AccordionView",
  "views/searchSelect/SearchSelectView",
], (Backbone, Semantic, Connector, AccordionView, SearchSelectView) => {
  const BASE_CLASS = "bioontology-browser";
  const CLASS_NAMES = {
    accordion: AccordionView.prototype.className,
    searchSelect: SearchSelectView.prototype.className,
    classInfo: "class-info",
    message: [
      Semantic.CLASS_NAMES.base,
      Semantic.CLASS_NAMES.variations.floating,
      Semantic.CLASS_NAMES.message.base,
      Semantic.CLASS_NAMES.variations.info,
    ],
    metacatui: {
      loader: "icon-spinner icon-spin icon-on-left",
      icon: "icon",
      iconLeft: "icon-on-left",
      floatRight: "pull-right",
      externalLinkIcon: "icon-external-link",
      addIcon: "icon-plus",
      tagIcon: "icon-tag",
    },
  };
  const BOOTSTRAP_CLASSES = {
    floatRight: "pull-right",
  };
  const BUTTON_ID = "bioontology-select-button";

  /**
   * @class BioontologyBrowser
   * @classdesc An interface to browser BioPortal ontologies and classes
   * @classcategory Views/Ontologies
   * @augments Backbone.View
   * @since 2.31.0
   * @screenshot views/ontologies/BioontologyBrowserView.png
   */
  const BioontologyBrowser = Backbone.View.extend(
    /** @lends BioontologyBrowser.prototype */
    {
      /** @inheritdoc */
      type: "BioontologyBrowser",

      /** @inheritdoc */
      className: BASE_CLASS,

      /**
       * The HTML string to display when no term is selected
       */
      noTermHTML: `
        <div class="${CLASS_NAMES.message.join(" ")}">
          <div class="${Semantic.CLASS_NAMES.message.header}">
            No term selected
          </div>
          <p>
            Select a term from the ontology to view details.
          </p>
        </div>`,

      events: {
        [`click #${BUTTON_ID}`]: "handleSelectButtonClick",
      },

      /**
       * Given variables, returns the HTML string the panel that shows details
       * about a class
       * @param {object} vars - The variables to use in the template
       * @param {string} vars.label - The label of the class
       * @param {string} vars.description - The description of the class
       * @param {string} vars.id - The ID of the class
       * @param {string} vars.uiLink - The link to the class on BioPortal
       * @returns {string} The HTML string for the class info panel
       */
      classInfoTemplate(vars) {
        return `<div class="${Semantic.CLASS_NAMES.base} ${Semantic.CLASS_NAMES.variations.raised} ${Semantic.CLASS_NAMES.card.base}" style="width:100%">
          <div class="content">
            
            <div class="header">
              <i class="${CLASS_NAMES.metacatui.icon} ${CLASS_NAMES.metacatui.iconLeft} ${CLASS_NAMES.metacatui.tagIcon} ${BOOTSTRAP_CLASSES.floatRight}"></i>
              ${vars.label}
            </div>
            <div class="${Semantic.CLASS_NAMES.card.meta}">
              ${vars.id}
            </div>
            <div class="${Semantic.CLASS_NAMES.card.description}">
              ${vars.description}
              <a href="${vars.uiLink}" target="_blank">
              More details on BioPortal <i class="${CLASS_NAMES.metacatui.icon} ${CLASS_NAMES.metacatui.externalLinkIcon}"></i>
            </a>
            </div>
          </div>
          <div class="${Semantic.CLASS_NAMES.base} ${Semantic.CLASS_NAMES.variations.attached} ${Semantic.CLASS_NAMES.colors.blue} ${Semantic.CLASS_NAMES.button.base}" id="${BUTTON_ID}">
            <i class="${CLASS_NAMES.metacatui.icon} ${CLASS_NAMES.metacatui.addIcon}" style="color:inherit";></i>
            Select term
          </div>
        </div>`;
      },

      /**
       * Extra CSS styles for this view.
       */
      styles: `
        .${BASE_CLASS} {
          border-radius: 5px;
          box-sizing: border-box;
          height: 100%;
          display: flex;
          flex-direction: column;
          max-height: 70vh;
          overflow: hidden;
        }

        .${BASE_CLASS} .${CLASS_NAMES.accordion} {
          overflow: auto;
          padding: 0rem 0.2rem;
        }

        .${BASE_CLASS} .${CLASS_NAMES.searchSelect} {
          box-shadow: 0 0.0.8rem 0.6rem 0 rgba(0, 0, 0, 0.3);
          border-bottom: 1px solid #d4d4d5;
          border-radius: 5px;
          z-index: 1;
        }

        .${BASE_CLASS} .${Semantic.CLASS_NAMES.card.base}, .${BASE_CLASS} .${Semantic.CLASS_NAMES.message.base} {
          transition: 0.3s ease-in-out;
          box-shadow: 0 -0.08rem 0.6rem 0 rgba(0, 0, 0, 0.3);
        }
      `,

      /**
       * Initialize the BioontologyBrowser view
       * @param {object} options - Options for the view
       * @param {object} options.ontologyOptions - The ontologies (or classes) that
       * a user can select terms from. Each ontology should have a label and an
       * ontology acronym, and an optional subtree root.
       * @example
       * const browser = new BioontologyBrowser({
       *  ontologyOptions: [
       *   {
       *     label: "Measurement Types (ECSO)",
       *     ontology: "ECSO",
       *     subTree: "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#MeasurementType"
       *    }, { ... } ]
       * });
       */
      initialize(options) {
        MetacatUI.appModel.addCSS(this.styles, "BioontologyBrowserStyles");
        const ontologyOptions = options?.ontologyOptions;
        const modelOptions = ontologyOptions ? { ontologyOptions } : {};
        this.model = new Connector(modelOptions);
        this.listenTo(
          this.model,
          "change:selectedClass",
          this.updateClassInfoEl,
        );
      },

      /** @inheritdoc */
      render() {
        this.el.innerHTML = "";

        // Ontology dropdown
        this.ontologySwitcher = new SearchSelectView({
          model: this.model.get("searchSelect"),
        });
        this.el.appendChild(this.ontologySwitcher.render().el);

        // Accordion with browseable ontology classes
        this.accordionView = new AccordionView({
          model: this.model.get("accordion"),
        });
        this.el.appendChild(this.accordionView.render().el);
        this.model.get("bioontology").fetch({ replaceCollection: true });

        // Panel with class details
        this.classInfoEl = document.createElement("div");
        this.el.appendChild(this.classInfoEl);
        this.updateClassInfoEl();
      },

      /**
       * Update the class info panel with the currently selected class
       * @param {number} transitionTime - The time in milliseconds for the fade
       * transition between changing info in the panel
       */
      updateClassInfoEl(transitionTime = 300) {
        let newHtml = this.noTermHTML;

        const ontClass = this.model.get("selectedClass");
        if (ontClass) {
          newHtml = this.classInfoTemplate({
            label: ontClass.get("prefLabel"),
            description: ontClass.get("definition"),
            id: ontClass.get("@id"),
            uiLink: ontClass.get("links")?.ui,
          });
        }

        // Change the content with a fade transition
        this.classInfoEl.style.transition = `opacity ${transitionTime}ms ease-in-out`;
        this.classInfoEl.style.opacity = "0.6";
        setTimeout(() => {
          this.classInfoEl.innerHTML = newHtml;
          setTimeout(() => {
            this.classInfoEl.style.opacity = "1";
          }, 1);
        }, transitionTime);
      },

      /**
       * Called when the select button is clicked and triggers the selected
       * event with the selected class model as the argument
       * @fires BioontologyBrowser#selected
       */
      handleSelectButtonClick() {
        const selectedClass = this.model.get("selectedClass");
        if (selectedClass) {
          this.trigger("selected", selectedClass);
        } else {
          // Show a message that no class is selected
          this.updateClassInfoEl();
        }
      },
    },
  );

  return BioontologyBrowser;
});