Source: src/js/views/versionHistory/ObjectVersionsView.js

/**
 * Backbone view responsible for rendering the list (<ul>) of individual
 * object versions that belong to a specific timeline group.
 */
define([
  "backbone",
  "collections/DataONEObjects",
  "views/versionHistory/ObjectVersionView",
], (Backbone, DataONEObjects, ObjectVersionView) => {
  "use strict";

  const BASE_CLASS = "object-versions";

  /**
   * CSS class names used throughout the ObjectVersionsView.
   * @enum {string}
   */
  const CLASS_NAMES = {
    base: BASE_CLASS,
  };

  /**
   * @class ObjectVersionsView
   * @classdesc A view that renders a list of object versions as <li> elements.
   * @classcategory Views/VersionHistory
   * @augments Backbone.View
   * @screenshot views/versionHistory/ObjectVersionsView.png
   * @since 2.37.0
   */
  const ObjectVersionsView = Backbone.View.extend(
    /** @lends ObjectVersionsView.prototype */ {
      /**
       * Identifier used when the application inspects view types.
       * @type {string}
       */
      type: "ObjectVersionsView",

      /** @inheritdoc */
      tagName: "ul",

      /** @inheritdoc */
      className: CLASS_NAMES.base,

      /**
       * @param {object} options Options passed to the view.
       * @param {DataONEObjects} options.collection Collection containing the versions to render.
       */
      initialize(options = {}) {
        this.collection =
          options.collection instanceof DataONEObjects
            ? options.collection
            : new DataONEObjects();
        this.subViews = [];
        this.subViewsByCid = new Map();
        this.referencePid = options.referencePid || null;
      },

      /**
       * Hook for future template customizations; currently unused placeholder.
       * @param {DataONEObjects} _collection The DataONEObjects collection.
       * @returns {string} HTML string
       */
      template(_collection) {
        return "";
      },

      /**
       * Swap the backing collection and refresh bindings.
       * @param {DataONEObjects} newCollection The collection to replace the
       * current one.
       */
      changeCollection(newCollection) {
        if (this.collection === newCollection) return;
        this.stopListeningCollection();
        this.collection = newCollection;
        this.render();
      },

      /**
       * Listens to collection events to trigger re-rendering.
       */
      listenToCollection() {
        this.stopListeningCollection();
        this.listenTo(
          this.collection,
          "add remove update sort reset",
          this.syncViews,
        );
      },

      /**
       * Stops listening to collection events.
       */
      stopListeningCollection() {
        if (this.collection) {
          this.stopListening(this.collection);
        }
      },

      /**
       * Renders <li> children for each DataONE object version.
       * @returns {this} The view instance
       */
      render() {
        this.listenToCollection();
        this.el.innerHTML = this.template(this.model);
        this.syncViews();
        return this;
      },

      /**
       * Ensure row subviews match the collection contents and order.
       * Reuses existing row views to avoid rebuilding the full list on each
       * incremental collection update.
       * @returns {this} The view instance.
       */
      syncViews() {
        if (!this.collection) return this;

        const activeCids = new Set(this.collection.map((model) => model.cid));
        this.subViewsByCid.forEach((subView, cid) => {
          if (!activeCids.has(cid)) {
            if (typeof subView.onClose === "function") {
              subView.onClose();
            }
            subView.remove();
            this.subViewsByCid.delete(cid);
          }
        });

        const fragment = document.createDocumentFragment();
        const newSubViews = [];
        const viewsNeedingInitialRender = [];

        this.collection.each((model) => {
          let subView = this.subViewsByCid.get(model.cid);
          if (!subView) {
            subView = new ObjectVersionView({
              model,
              referencePid: this.referencePid,
            });
            this.subViewsByCid.set(model.cid, subView);
            viewsNeedingInitialRender.push(subView);
          } else if (subView.model !== model) {
            subView.changeModel(model);
          }

          fragment.appendChild(subView.el);
          newSubViews.push(subView);
        });

        this.el.appendChild(fragment);
        viewsNeedingInitialRender.forEach((subView) => subView.render());
        this.subViews = newSubViews;

        return this;
      },

      /** Show or hide sub-views based on their model's hiddenByUI attribute */
      applyVisibilityState() {
        this.subViews.forEach((subView) => subView.applyVisibilityState?.());
      },

      /**
       * Manually add tooltips to sub-views. Useful if they've been rendered
       * while not in the DOM.
       */
      addTooltips() {
        this.subViews.forEach((subView) => subView.addTooltips?.());
      },

      /**
       * Cleans up and removes all the ObjectVersionView subviews.
       */
      removeSubViews() {
        this.subViewsByCid.forEach((subView) => {
          if (typeof subView.onClose === "function") {
            subView.onClose();
          }
          subView.remove();
        });
        this.subViewsByCid.clear();
        this.subViews = [];
      },

      /**
       * Stops collection listeners prior to removal.
       */
      onClose() {
        this.removeSubViews();
        this.stopListeningCollection();
      },

      /** @inheritdoc */
      remove() {
        this.onClose();
        return Backbone.View.prototype.remove.call(this);
      },
    },
  );

  return ObjectVersionsView;
});