susimail/src/js/deleteMail.js

/**
 * @module deleteMail
 * @file I2P+ SusiMail delete-mail modal handler.
 * Ensures the delete-mail confirmation modal is correctly positioned and
 * visible on instantiation, with iframe-aware viewport calculations
 * and a MutationObserver for cleanup on removal.
 * @author dr|z3d
 * @license AGPL3 or later
 */

/**
 * IIFE that manages the lifecycle of the delete-mail modal dialog.
 * Handles positioning, overlay creation, resize events, and automatic
 * cleanup when the modal element is removed from the DOM.
 * @function handleModal
 * @returns {void}
 */
(function handleModal() {
  let overlay = document.getElementById("overlay");

  /**
   * Toggles modal display state by positioning the `#nukemail` dialog,
   * managing body scroll classes, and setting up resize and mutation
   * observers for dynamic cleanup.
   *
   * @async
   * @function toggleModalStyles
   * @returns {Promise<void>}
   */
  async function toggleModalStyles() {
    if (!overlay) { overlay = await createOverlay(); }

    const modal = document.getElementById("nukemail");
    const parentDoc = window.parent.document;
    const iframe = parentDoc.getElementById("susimailframe");
    let iframed = document.documentElement.classList.contains("iframed") || window.top != window.parent.top;
    let modalRect;
    let modalheight;
    let viewportHeight;

    if (modal) {
      viewportHeight = window.parent.innerHeight;
      modalRect = modal.getBoundingClientRect();
      modalHeight = modalRect.height;
      document.body.classList.add("modal");
      parentDoc.body.classList.add("modal");
      modal.style.top = (viewportHeight - modalHeight) / 3 + "px";
      window.scrollTo(0,0);
      if (iframed) {
        window.parent.scrollTo(0,0);
        iframe.style.maxHeight = (viewportHeight - 80) + "px";
        overlay.style.maxHeight = (viewportHeight - 88) + "px";
      } else {
        overlay.style.maxHeight = "100vh";
        document.body.style.height = "calc(100vh - 14px)";
      }
    } else {
      document.body.classList.remove("modal");
      document.body.style.overflow = null;
      document.body.style.height = null;
      if (iframed) {
        iframe.style.maxHeight = null;
        parentDoc.body.classList.remove("modal");
      }
    }

    const observer = new MutationObserver(async mutationsList => {
      let modalRemoved = false;
      for (const mutation of mutationsList) {
        if (mutation.type === "childList") {
          const modal = document.getElementById("nukemail");
          if (!modal) {
            modalRemoved = true;
            break;
          }
        }
      }
      if (modalRemoved) {
        document.body.style.overflow = null;
        iframed = document.documentElement.classList.contains("iframed");
        if (iframed) {iframe.style.maxHeight = null;}
        document.body.classList.remove("modal");
        if (iframed) {parentDoc.body.classList.remove("modal");}
      }
    });

    observer.observe(document.body, { childList: true, subtree: true, attributes: true });
    window.addEventListener("resize", () => {
      if (modal) {
        viewportHeight = window.parent.innerHeight;
        modal.style.top = "5%";
        if (parseInt(viewportHeight, 10) > 500) {
          modal.style.top = (viewportHeight - modalHeight) / 3 + "px";
        }
        if (iframed) {
          iframe.style.maxHeight = (viewportHeight - 80) + "px";
          overlay.style.maxHeight = (viewportHeight - 88) + "px";
        } else {
          document.body.style.maxHeight = "calc(100vh - 14px)";
          overlay.style.maxHeight = "100vh";
        }
      } else {
        document.body.style.maxHeight = null;
        if (iframed) {
          iframe.style.maxHeight = null;
          parentDoc.body.classList.remove("modal");
        }
      }
    }, {passive: true});
    document.addEventListener("DOMContentLoaded", toggleModalStyles);
  };
  toggleModalStyles();
})();

/**
 * Creates and appends a fullscreen overlay `<div>` to the document body.
 * @async
 * @function createOverlay
 * @returns {Promise<HTMLElement>} The created overlay element.
 */
async function createOverlay() {
  const layer = document.createElement("div");
  layer.id = "overlay";
  document.body.appendChild(layer);
  return layer;
}