Source: src/js/views/accordion/AccordionView.js

define([
  "jquery",
  "backbone",
  "semantic",
  "views/accordion/AccordionItemView",
  "models/accordion/Accordion",
], ($, Backbone, Semantic, AccordionItemView, AccordionModel) => {
  // The base class for the view
  const BASE_CLASS = "accordion-view";

  /**
   * @class AccordionView
   * @classdesc An extension of the Semantic UI accordion that allows for
   * defining contents with a Backbone model, and adds tooltips and other
   * features.
   * @classcategory Views/Accordion
   * @augments Backbone.View
   * @class
   * @since 2.31.0
   * @screenshot views/accordion/AccordionView.png
   */
  const AccordionView = Backbone.View.extend(
    /** @lends AccordionView.prototype */
    {
      /** @inheritdoc */
      type: "AccordionView",

      /** @inheritdoc */
      className: BASE_CLASS,

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

      /**
       * Initializes the AccordionView with the model and listens for changes
       * to the items collection.
       * @param {object} options - Options for the view
       * @param {Accordion} [options.model] - The Accordion model for the view. If
       * not provided, a new Accordion model will be created.
       * @param {object} [options.modelData] - Optional data to initialize the
       * Accordion model with. Only used if options.model is not provided.
       */
      initialize(options) {
        // Set the model to the provided model or create a new one
        this.model = options?.model || new AccordionModel(options?.modelData);

        this.listenTo(this.model.get("items"), "add", this.addNewItem);
        this.listenTo(this.model.get("items"), "remove", this.removeItem);
      },

      /** @inheritdoc */
      render() {
        this.initializeAccordion();

        // Start rendering the root, and then the children will be rendered
        // recursively
        const rootItems = this.model.getRootItems();
        const rootAccordion = this.createAccordion(rootItems);
        this.rootAccordion = rootAccordion;

        this.$el.append(rootAccordion);

        return this;
      },

      /**
       * Initializes the Semantic UI accordion module with the settings from the
       * model. The module handles applying accordion behavior to the view and
       * any new DOM elements that are added.
       */
      initializeAccordion() {
        const view = this;

        // Gather all the necessary settings from the model
        const modelJSON = this.model.toJSON();
        const settings = Object.fromEntries(
          Object.entries(modelJSON).filter(([key, _]) =>
            Semantic.ACCORDION_SETTINGS_KEYS.includes(key),
          ),
        );

        // Pass the associated model and view to the callback functions instead
        // only having access to the DOM element as the context
        const createCallback = (callbackName) => {
          const originalCallback = this.model.get(callbackName);
          if (originalCallback) {
            return function callbackWrapper() {
              const contentEl = this[0];
              const itemView = view.viewFromContentEl(contentEl);
              const itemModel = itemView.model;
              originalCallback(itemModel, itemView);
            };
          }
          return null;
        };
        Semantic.ACCORDION_CALLBACKS.forEach((callbackName) => {
          const callback = createCallback(callbackName);
          if (callback) {
            settings[callbackName] = callback;
          } else {
            delete settings[callbackName];
          }
        });

        // Initialize the accordion with the specified settings
        this.$el.accordion(settings);
      },

      /**
       * @param {HTMLElement} contentEl A content element from an item
       * @returns {AccordionItemView} The view associated with the item
       */
      viewFromContentEl(contentEl) {
        const views = Object.values(this.itemViews);
        return views.find((view) => view.contentContainer === contentEl);
      },

      /**
       * Creates a container for the accordion with the necessary classes for
       * Semantic UI and renders any items belonging to the accordion. This can
       * be used to create the root accordion or nested accordions.
       * @param {AccordionItem[]} items - An array of AccordionItem models
       * @returns {HTMLElement} The container element for the accordion
       */
      createAccordion(items) {
        const accordionContainer = this.createContainer();
        if (items?.length) this.addItems(items, accordionContainer);
        return accordionContainer;
      },

      /**
       * Creates a container element for the accordion with the necessary
       * classes
       * @returns {HTMLElement} The container element for the accordion
       */
      createContainer() {
        const container = document.createElement("div");
        container.classList.add(
          Semantic.CLASS_NAMES.accordion.container,
          Semantic.CLASS_NAMES.base,
        );
        container.style.marginTop = 0;

        // Add optional class names based on model properties
        const optionalClasses = ["fluid", "styled", "inverted"];
        optionalClasses.forEach((className) => {
          if (this.model.get(className)) {
            container.classList.add(Semantic.CLASS_NAMES.variations[className]);
          }
        });

        return container;
      },

      /**
       * Adds items to the container element for the accordion
       * @param {AccordionItem[]} models - An array of AccordionItem models
       * @param {HTMLElement} container - The container element for the
       * accordion
       */
      addItems(models, container) {
        models.forEach((model) => {
          this.addItem(model, container);
        });
      },

      /**
       * Adds an item to the container element for the accordion
       * @param {AccordionItem} model - An AccordionItem model
       * @param {HTMLElement} container - The container element for the
       * accordion
       */
      addItem(model, container) {
        const itemView = new AccordionItemView({ model }).render();

        // Semantic UI expects the title and content to be direct children of
        // the accordion container, important for correct application of CSS
        container.appendChild(itemView.titleContainer);
        container.appendChild(itemView.contentContainer);

        if (!this.itemViews) this.itemViews = {};

        const id = model.get("itemId");
        this.itemViews[id] = itemView;
        // Add children if they exist, otherwise just re-adds the content
        this.refreshContent(id);
      },

      /**
       * Refreshes the content of an item in the accordion. If the item has
       * children, it will create a new accordion with the children. Otherwise,
       * it will update the content with the item's model content attribute.
       * @param {string} itemId - The model itemId for the item
       */
      refreshContent(itemId) {
        // Check if there are children for the given item
        const children = this.model.getChildren(itemId);
        const itemView = this.itemViews?.[itemId];
        if (!itemView) return;
        if (children?.length) {
          const subAccordion = this.createAccordion(children);
          itemView.updateContent(subAccordion);
        } else {
          itemView.updateContent(itemView.model.get("content"));
        }
      },

      /**
       * Handles adding a new item to the accordion when the items collection
       * has new models added to it.
       * @param {AccordionItem} model - The new AccordionItem model
       */
      addNewItem(model) {
        const parent = model.get("parent");
        if (parent) {
          this.refreshContent(parent);
        } else {
          this.addItem(model, this.rootAccordion);
        }
      },

      /**
       * Handles removing an item from the accordion when the items collection
       * has models removed from it.
       * @param {AccordionItem} model - The removed AccordionItem model
       */
      removeItem(model) {
        const id = model.get("itemId") || model.cid;
        const itemView = this.itemViews[id];
        // remove the item view from the itemViews object
        delete this.itemViews[id];
        itemView?.remove();
      },

      /**
       * Removes all items from the accordion and clears the itemViews object.
       */
      clearAllItems() {
        Object.entries(this.itemViews).forEach(([id, view]) => {
          view.remove();
          delete this.itemViews[id];
        });
      },
    },
  );

  return AccordionView;
});