Source: blocks.js

/** A ContainerBlock is a container for other blocks (including other ContainerBlocks). */
class ContainerBlock {
  /**
  * Create a container block.
  * @param {Object} options - Configuration options.
  * @author Lawrence Fyfe
  */
  constructor(options) {
    this.blockType = "container";
    this.direction = options?.direction ?? "column";
    this.blockArray = [];
    this.parentElement;
  }

  /**
  * Add the specified block to this block.
  * @param {block} block - The block to add.
  */
  addBlock(block) {
    this.blockArray.push(block);
  }

  /**
  * Initialize this block by attaching it to the specified parent element.
  * @param {element} parentElement - The parent element.
  */
  initialize(parentElement) {
    this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
  }

  /**
  * Draw this block.
  */
  draw() {
    if (this.parentElement !== undefined) {
      this.containerBlockDiv = this.parentElement.append("div")
        .attr("class", "containerBlockDiv")
        .style("display", "flex")
        .style("flex-direction", this.direction);

      for (let i = 0; i < this.blockArray.length; i++) {
        this.blockArray[i].parentElement = this.containerBlockDiv;
        this.blockArray[i].draw();
      }
    }
  }

  /**
  * Erase this block.
  */
  erase() {
    if (this.containerBlockDiv !== undefined) {
      this.containerBlockDiv.selectAll("*").remove();
    }
  }
}

/** An PanelBlock is a container for panels. */
class PanelBlock {
  /**
  * Create a panel block.
  * @author Lawrence Fyfe
  */
  constructor() {
    this.blockType = "panel";
    this.paneArray = [];
    this.singleCurrentPane;
    this.parentElement;
    this.initialized = false;
  }

  /**
  * Add the specified pane to this block.
  * @param {pane} pane - The pane to add.
  */
  addPane(pane) {
    this.paneArray.push(pane);
  }

  /**
  * Set the specified pane to be the single current pane for this block. This method is used when there are multiple layered panel panes
  * in a block and only one panel can be shown at a time.
  * @param {pane} pane - The pane to set as the single current pane.
  */
  setSingleCurrentPane(pane) {
    if (this.paneArray.find((element) => element === pane) !== undefined) {
      this.singleCurrentPane = pane;
    }
  }

  /**
  * Initialize this block by attaching it to the specified parent element.
  * @param {element} parentElement - The parent element.
  */
  initialize(parentElement) {
    this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);

    this.panelBlockDiv = this.parentElement.append("div")
      .attr("class", "panelBlockDiv");

    this.initialized = true;
  }

  /**
  * Draw this block.
  */
  draw() {
    if (this.initialized) {
      this.erase();

      for (let i = 0; i < this.paneArray.length; i++) {
        this.paneArray[i].initializePanel(this.panelBlockDiv);
      }

      if (this.initialized) {
        if (this.singleCurrentPane !== undefined) {
          this.singleCurrentPane.drawPanel();
        }
        else {
          for (let i = 0; i < this.paneArray.length; i++) {
            this.paneArray[i].drawPanel();
          }
        }
      }
    }
  }

  /**
  * Erase this block.
  */
  erase() {
    for (let i = 0; i < this.paneArray.length; i++) {
      this.paneArray[i].panelOn = false;
    }
    if (this.panelBlockDiv !== undefined) {
      this.panelBlockDiv.selectAll("*").remove();
    }
  }
}

/** An SVG image block contains an SVG image. */
class SVGImageBlock {
  /**
  * Create a block for an SVG image.
  * @param {string} file - The SVG file name String.
  * @author Lawrence Fyfe
  */
  constructor(file) {
    this.blockType = "svgImage";
    this.file = file;
    this.svg;
    this.svgWidth;
    this.svgHeight;
    this.parentElement;
    this.width;
    this.height;
    this.initialized = false;

    this.fileIsReady = d3.svg(this.file).then((svg) => {
      this.svg = svg.documentElement;
      this.svgWidth = svg.documentElement.getAttribute("width");
      this.svgHeight = svg.documentElement.getAttribute("height");
    });
  }

  /**
  * Determines whether the SVG file specified for this block as been fetched.
  */
  async isReady() {
    await this.fileIsReady;
  }

  /**
  * Initialize this block with the specified parameters.
  * @param {element} parentElement - The parent element.
  * @param {number} width - The width to use for the SVG image.
  * @param {number} height - The height to use for the SVG image.
  */
  initialize(parentElement, width, height) {
    this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
    this.width = width;
    this.height = height;

    this.svgImageBlockDiv = this.parentElement.append("div")
      .attr("class", "svgImageBlockDiv");

    this.initialized = true;
  }

  /**
  * Draw this block.
  */
  draw() {
    if (this.initialized) {
      this.fileIsReady.then(() => {
        this.erase();
        this.svgImageBlockDiv.append(() => this.svg)
          .attr("class", "svgImage")
          .attr("x", "0")
          .attr("y", "0")
          .attr("width", this.width)
          .attr("height", this.height);
      });
    }
  }

  /**
  * Erase this block.
  */
  erase() {
    if (this.svgImageBlockDiv !== undefined) {
      this.svgImageBlockDiv.select("svg").remove();
    }
  }
}