Source: src/js/views/portals/editor/PortEditorImageView.js

define([
  "underscore",
  "jquery",
  "backbone",
  "models/portals/PortalImage",
  "views/ImageUploaderView",
  "text!templates/imageEdit.html",
], function (_, $, Backbone, PortalImage, ImageUploaderView, Template) {
  /**
   * @class PortEditorImageView
   * @classdesc A view that allows the user to upload an image as a DataONEObject
   * @classcategory Views/Portals/Editor
   * @extends Backbone.View
   */
  var PortEditorImageView = Backbone.View.extend(
    /** @lends PortEditorImageView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "PortEditorImage",

      /**
       * The HTML tag name to use for this view's element
       * @type {string}
       */
      tagName: "div",

      /**
       * The HTML classes to use for this view's element
       * @type {string}
       */
      className: "edit-image",

      /**
       * A jQuery selector for the element that the ImageUploaderView should be inserted into
       * @type {string}
       */
      imageUploaderContainer: ".image-uploader-container",

      /**
       * The ImageUploaderView created and used by this ImageEdit view.
       * @type {ImageUploader}
       */
      uploader: undefined,

      /**
       * The PortalImage model that is being edited
       * @type {Image}
       */
      model: undefined,

      /**
       * The Portal model that contains the PortalImage
       * @type {Portal}
       */
      parentModel: undefined,

      /**
       * A reference to the PortalEditorView
       * @type {PortalEditorView}
       */
      editorView: undefined,

      /**
       * The maximum height of the image preview. If set to false,
       * no css width property is set.
       * @type {number}
       */
      imageHeight: 150,

      /**
       * The display width of the image preview. If set to false,
       * no css width property is set.
       * @type {number|boolean}
       */
      imageWidth: 150,

      /**
       * The minimum required height of the image file. If set, the uploader will
       * reject images that are shorter than this. If null, any image height is
       * accepted.
       * @type {number}
       */
      minHeight: null,

      /**
       * The minimum required height of the image file. If set, the uploader will
       * reject images that are shorter than this. If null, any image height is
       * accepted.
       * @type {number}
       */
      minWidth: null,

      /**
       * The maximum height for uploaded files. If a file is taller than this, it
       * will be resized without warning before being uploaded. If set to null,
       * the image won't be resized based on height (but might be depending on
       * maxWidth).
       * @type {number}
       */
      maxHeight: null,

      /**
       * The maximum width for uploaded files. If a file is wider than this, it
       * will be resized without warning before being uploaded. If set to null,
       * the image won't be resized based on width (but might be depending on
       * maxHeight).
       * @type {number}
       */
      maxWidth: null,

      /**
       * Text to instruct the user how to upload an image
       * @type {string[]}
       */
      imageUploadInstructions: ["Drag & drop an image or click here to upload"],

      /**
       * Label for the first text input where the user enters the ImageModel label.
       * If this is set to false, the label input will not be shown.
       * @type {string|boolean}
       */
      nameLabel: "Name",

      /**
       * Label for the second text input where the user enters the ImageModel
       * associated URL. If this is set to false, the URL input will not be shown.
       * @type {string|boolean}
       */
      urlLabel: "URL",

      /**
       * The HTML tag name to insert the uploaded image into. Options are "img",
       * in which case the image is inserted as an HTML <img>, or "div", in which
       * case the image is inserted as the background of a "div".
       * @type {string}
       */
      imageTagName: "div",

      /**
       * Whether or not a remove button should be shown.
       * @type {boolean}
       */
      removeButton: false,

      /**
       * References to templates for this view. HTML files are converted to Underscore.js templates
       */
      template: _.template(Template),

      /**
       * The events this view will listen to and the associated function to call.
       * @type {Object}
       */
      events: {
        "mouseover .toggle-remove-preview": "showRemovePreview",
        "mouseout  .toggle-remove-preview": "hideRemovePreview",
        "click .remove-image-edit-view": "removeSelf",
        "focusout .basic-text": "redoValidation",
      },

      /**
       * Creates a new PortEditorImageView
       * @param {Object} options - A literal object with options to pass to the view
       * @property {Portal}  options.parentModel - Gets set as PortEditorImageView.parentModel
       * @property {PortalEditorView}  options.editorView - Gets set as PortEditorImageView.editorView
       * @property {PortalImage}  options.model - Gets set as PortEditorImageView.model
       * @property {string[]}  options.imageUploadInstructions - Gets set as ImageUploaderView.imageUploadInstructions
       * @property {string}  options.nameLabel - Gets set as PortEditorImageView.nameLabel
       * @property {string}  options.urlLabel - Gets set as PortEditorImageView.urlLabel
       * @property {string}  options.imageTagName - Gets set as ImageUploaderView.imageTagName
       * @property {string}  options.removeButton - Gets set as ImageUploaderView.removeButton
       * @property {number}  options.imageWidth - Gets set as ImageUploaderView.width
       * @property {number}  options.imageHeight - Gets set as ImageUploaderView.height
       * @property {number}  options.minWidth - Gets set as ImageUploaderView.minWidth
       * @property {number}  options.minHeight - Gets set as ImageUploaderView.minHeight
       * @property {number}  options.maxWidth - Gets set as ImageUploaderView.maxWidth
       * @property {number}  options.maxHeight - Gets set as ImageUploaderView.maxHeight
       */
      initialize: function (options) {
        try {
          if (typeof options == "object") {
            this.parentModel = options.parentModel;
            this.editorView = options.editorView;
            this.model = options.model;
            this.imageUploadInstructions = options.imageUploadInstructions;
            this.imageWidth = options.imageWidth;
            this.imageHeight = options.imageHeight;
            this.nameLabel = options.nameLabel;
            this.urlLabel = options.urlLabel;
            this.imageTagName = options.imageTagName;
            this.removeButton = options.removeButton;
            this.minHeight = options.minHeight;
            this.minWidth = options.minWidth;
            this.maxHeight = options.maxHeight;
            this.maxWidth = options.maxWidth;
          }

          if (!this.model) {
            this.model = new PortalImage();
          }

          //If an alternative repo is configured, upload to that alt repo
          let useAltRepo = MetacatUI.appModel.getActiveAltRepo() ? true : false;
          this.model.set("useAltRepo", useAltRepo);
        } catch (e) {
          console.log(
            "PortEditorImageView failed to initialize. Error message: " + e,
          );
        }
      },

      /**
       * Renders this view
       */
      render: function () {
        try {
          // Reference to this view
          var view = this;

          //Insert the template for this view
          this.$el.html(
            this.template({
              nameLabel: this.nameLabel,
              urlLabel: this.urlLabel,
              nameText: this.model.get("label"),
              urlText: this.model.get("associatedURL"),
              removeButton: this.removeButton,
            }),
          );

          // Create an ImageUploaderView and insert into this view. Allow it to be
          // accessed from parent views.
          this.uploader = new ImageUploaderView({
            model: this.model,
            url: this.model.get("imageURL"),
            uploadInstructions: this.imageUploadInstructions,
            imageTagName: this.imageTagName,
            height: this.imageHeight,
            width: this.imageWidth,
            minHeight: this.minHeight,
            minWidth: this.minWidth,
            maxHeight: this.maxHeight,
            maxWidth: this.maxWidth,
          });
          this.$(this.imageUploaderContainer).append(this.uploader.el);
          this.uploader.render();

          // Reset image attributes when user removes image
          this.stopListening(this.uploader, "removedfile");
          this.listenTo(this.uploader, "removedfile", function () {
            var defaults = view.model.defaults();
            view.model.set("identifier", defaults.identifier);
            view.model.set("imageURL", defaults.imageURL);
            view.redoValidation();
          });

          // Try to validate again when image is added but not yet uploaded
          this.stopListening(this.uploader, "addedfile");
          this.listenTo(this.uploader, "addedfile", function () {
            view.redoValidation();
          });

          // Update the PortalImage model when the image is successfully uploaded
          this.stopListening(this.uploader, "successSaving");
          this.listenTo(
            this.uploader,
            "successSaving",
            function (dataONEObject) {
              view.model.set("identifier", dataONEObject.get("id"));
              view.model.set("imageURL", dataONEObject.url());
              view.redoValidation();
            },
          );

          this.listenTo(
            this.model,
            "change:associatedURL",
            this.showValidation,
          );

          // Allows model to update when user types in text field
          this.$el.find(".basic-text").data({ model: this.model, view: this });

          //Initialize any tooltips
          this.$(".tooltip-this").tooltip();

          //Save a reference to this view
          this.$el.data("view", this);
        } catch (e) {
          console.log("ImageEdit view not rendered, error message: " + e);
        }
      },

      /**
       * removeSelf - Removes this ImageEdit view and the associated PortalImage
       * model from the parent Portal model.
       */
      removeSelf: function () {
        try {
          var view = this;

          // Remove the model
          this.parentModel.removePortalImage(this.model);
          // Remove the view
          this.$el.animate(
            { width: "0px", overflow: "hidden" },
            {
              duration: 250,
              complete: function () {
                view.onClose();
                view.remove();
              },
            },
          );
        } catch (e) {
          console.log(
            "Failed to remove an ImageEdit view. Error message: " + e,
          );
        }
      },

      /**
       * redoValidation - Called when a user focuses out of input fields
       * with the .basic-text class (organization name and associated URL), or
       * when an image is successfully uploaded or removed. This function
       * validates the PortalImage model again and shows errors if there are any.
       */
      redoValidation: function () {
        try {
          view = this;
          // Add a small pause so that the model is updated first.
          setTimeout(function () {
            view.removeValidation();
            view.showValidation();
          }, 1);
        } catch (e) {
          console.log(e);
        }
      },

      /**
       * showValidation - show validation errors for this ImageEdit view
       */
      showValidation: function () {
        try {
          var errors = this.model.validate();

          if (errors) {
            _.each(
              errors,
              function (errorMsg, category) {
                var categoryEls = this.$("[data-category='" + category + "']");
                //Use the showValidationMessage function from the parent view
                if (this.editorView && this.editorView.showValidationMessage) {
                  this.editorView.showValidationMessage(categoryEls, errorMsg);
                }
              },
              this,
            );

            // add class to dropzone element if error has to do with image
            if (errors.identifier) {
              this.$el.find(".dropzone").addClass("error");
            }
          }
        } catch (e) {
          console.log("Failed to validate portalImage, error: " + e);
        }
      },

      /**
       * removeValidation - Remove displayed validation errors, if any
       */
      removeValidation: function () {
        this.$(".notification.error").removeClass("error").empty();
        this.$(
          ".section-link-container.error, input.error, textarea.error",
        ).removeClass("error");
        this.$(".validation-error-icon").hide();
        this.$el.find(".dropzone").removeClass("error");
      },

      /**
       * Add the "remove-preview" class which will show a preview for removing this image, via CSS
       */
      showRemovePreview: function () {
        try {
          this.$el.addClass("remove-preview");
        } catch (error) {
          console.error(
            "Failed to preview the removal of an image edit view. Error message: " +
              error,
          );
        }
      },

      /**
       * Removes the "remove-preview" class which will hide the preview for removing this image, via CSS
       */
      hideRemovePreview: function (e) {
        try {
          this.$el.removeClass("remove-preview");
        } catch (error) {
          console.error(
            "Failed to preview the removal of an image edit view. Error message: " +
              error,
          );
        }
      },

      /**
       * This function is called whenever this view is about to be removed from the page.
       */
      onClose: function () {
        //Destroy any tooltips in this view that are still open
        this.$(".tooltip-this").tooltip("destroy");
      },
    },
  );

  return PortEditorImageView;
});