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

define([
  "underscore",
  "jquery",
  "backbone",
  "bioportal",
  "models/metadata/eml211/EMLAnnotation",
  "views/AnnotationView",
  "text!templates/metadata/eml-measurement-type.html",
  "text!templates/metadata/eml-measurement-type-annotations.html",
], function (
  _,
  $,
  Backbone,
  BioPortal,
  EMLAnnotation,
  AnnotationView,
  EMLMeasurementTypeTemplate,
  EMLMeasurementTypeAnnotationsTemplate,
) {
  /**
   * @class EMLMeasurementTypeView
   * @classdec The EMLMeasurementTypeView is a view to render a specialized
   * EML annotation editor that lets the use pick a single annotation from a
   * specified ontology or portion of an ontology.
   * @classcategory Views/Metadata
   * @extends Backbone.View
   */
  var EMLMeasurementTypeView = Backbone.View.extend(
    /** @lends EMLMeasurementTypeView.prototype */ {
      tagName: "div",
      className: "eml-measurement-type row-fluid",
      template: _.template(EMLMeasurementTypeTemplate),
      annotationsTemplate: _.template(EMLMeasurementTypeAnnotationsTemplate),

      events: {
        "click .remove": "handleRemove",
        "change .notfound": "handleMeasurementTypeNotFound",
      },

      /**
       * Reference to the parent EMLAttribute model so we can .set/.get the
       * 'annotation' attribute when this view adds or removes annotations
       */
      model: null,

      /**
       * Whether to allow (true) the user to pick more than one value
       */
      multiSelect: false,

      /**
       * The ontology (on BioPortal) to show a tree for
       */
      ontology: "ECSO",

      /**
       * Which term within this.ontology to root the tree at. Set this to null
       * to start at the root of the tree and set it to a class/term URI
       * to root the tree at that class/term
       */
      startingRoot:
        "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#MeasurementType",

      /**
       * The label for the property that goes with terms selected with this
       * interface as well as the terms inserted
       */
      filterLabel: "contains measurements of type",

      /**
       * The URI of for the property that goes with terms selected with this
       * interface as well as the terms inserted
       */
      filterURI:
        "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#containsMeasurementsOfType",

      initialize: function (options) {
        this.model = options.model;
      },

      render: function () {
        var viewRef = this;

        // Do an initial render of the view
        this.$el.html(this.template());
        this.renderAnnotations();

        // Set up tree widget
        var tree = this.$(".measurement-type-browse-tree").NCBOTree({
          apikey: MetacatUI.appModel.get("bioportalAPIKey"),
          ontology: this.ontology,
          width: "400",
          startingRoot: this.startingRoot || "roots",
          jumpAfterSelect: true,
          selectFromAutocomplete: true,
          afterJumpToClass: function (cls) {
            var foundClass = viewRef
              .$("ul.ncboTree")
              .find("a[data-id='" + encodeURIComponent(cls) + "']");

            if (foundClass.length <= 0) {
              return;
            }

            foundClass[0].scrollIntoView(false);
          },
        });

        tree.on(
          "afterSelect",
          function (event, classId, prefLabel, selectedNode) {
            viewRef.selectConcept.call(
              viewRef,
              event,
              classId,
              prefLabel,
              selectedNode,
            );
          },
        );

        tree.init();
      },

      /**
       * Render just the list of annotation in the view
       *
       * Used in both render() to perform the initial render and by
       * selectConcept and handleRemove to update the list as annotations
       * are added and removed
       */
      renderAnnotations: function () {
        var viewRef = this;

        var filtered = _.filter(
          this.model.get("annotation"),
          function (annotation) {
            return annotation.get("propertyURI") === viewRef.filterURI;
          },
        );

        var templateData = {
          annotations: _.map(
            filtered,
            function (annotation) {
              return {
                propertyLabel: annotation.get("propertyLabel"),
                propertyURI: annotation.get("propertyURI"),
                valueLabel: annotation.get("valueLabel"),
                valueURI: annotation.get("valueURI"),
                contextString: "This attribute",
              };
            },
            this,
          ),
        };

        this.$(".measurement-type-annotations").html(
          this.annotationsTemplate(templateData),
        );

        // Create AnnotationViews for each Measurement Type so we have nice
        // popovers
        _.each(this.$(".annotation"), function (annoEl) {
          var view = new AnnotationView({ el: annoEl });

          view.render();
        });
      },

      /**
       * Add an annotation when the user selects on in the UI
       *
       * @param {Event} event - The click event handler
       * @param {string} classId - The selected term's URI
       * @param {string} prefLabel - The selected term's prefLabel
       * @param {Element} selectedNode - The clicked element
       */
      selectConcept: function (event, classId, prefLabel, selectedNode) {
        var anno = new EMLAnnotation({
          propertyLabel: this.filterLabel,
          propertyURI: this.filterURI,
          valueLabel: prefLabel,
          valueURI: classId,
        });

        if (!this.model.get("annotation")) {
          this.model.set("annotation", []);
        }

        // Append if we're in multi-select or we're adding an annotation
        // stating that the selected term is not specific enough
        if (this.multiSelect) {
          var annotations = this.model.get("annotation");
          annotations.push(anno);
        } else {
          // Remove any existing filtered annotations before pushing the new
          this.removeAnnotationsBy("propertyURI", this.filterURI);
          var annotations = this.model.get("annotation");
          annotations.push(anno);
        }

        this.model.set("annotation", annotations);

        // Ensure we have an id attribute because annotations require the
        // parent to have one
        if (!this.model.get("xmlID")) {
          this.model.createID();
        }

        this.model.trickleUpChange();

        // Force a re-render of the annotations
        this.renderAnnotations();
      },

      /**
       * Handle a click event to remove an annotation
       *
       * This method deletes by value rather than index because multiple
       * views may be managing the state of the annotation attribute for a given
       * EMLAttribute. i.e., the indices might not match when removals are
       * happening in both views.
       *
       * @param {Event} e - A click event handler
       */
      handleRemove: function (e) {
        // First we find the container div for the annotation so we can get
        // the values to match against from data properties
        var annotationEl = $(e.target).parents(".annotation");

        if (!annotationEl) {
          return;
        }

        var valueLabel = $(annotationEl).data("value-label"),
          valueURI = $(annotationEl).data("value-uri");

        if (!valueLabel || !valueURI) {
          return;
        }

        this.removeAnnotationsBy("valueURI", valueURI);
      },

      /**
       * Remove a annotations by value
       *
       * Removes all matching annotations with a matching valueURI
       *
       * @param {string} attribute - The model attribute to pull from
       * @param {string} value - The value to compare with
       */
      removeAnnotationsBy: function (attribute, value) {
        // Remove by index now that we've found the right one
        var existing = this.model.get("annotation");

        // Remove annotations matching the input
        var filtered = _.reject(
          existing,
          function (anno) {
            return anno.get(attribute) === value;
          },
          this,
        );

        this.model.set("annotation", filtered);
        this.model.trickleUpChange();

        // Force a re-render
        this.renderAnnotations();
      },

      /**
       * Handle when the user can't find a class for their attribute
       *
       * This method isn't fantastic. We need a way to signify that the user
       * couldn't find a good match for their attribute. EML doesn't have a way
       * to specify this scenario so we use a sentinel value here in the hopes
       * that moderation workflows will pick it up.
       *
       * @param {Event} e - The click event
       */
      handleMeasurementTypeNotFound: function (e) {
        if (e.target.checked) {
          this.removeAnnotationsBy("propertyURI", this.filterURI);
          this.$el.find(".measurement-type-browse").hide();
        } else {
          this.$el.find(".measurement-type-browse").show();
        }
      },
    },
  );

  return EMLMeasurementTypeView;
});