i2psnark/res/js/convertTooltips.js

/**
 * @module convertTooltips
 * @file convertTooltips.js - Convert title attributes to data-tooltips for enhanced styling.
 * @description Converts native browser title attributes on progress bar and torrent status
 * elements into custom data-tooltip attributes with CSS-based tooltip styling. Uses a
 * MutationObserver to handle dynamically added elements. Provides styled pseudo-element
 * tooltips with arrow indicators.
 * @author dr|z3d
 * @license AGPL3 or later
 */

/**
 * @description Self-executing IIFE that initializes the tooltip conversion system.
 * Converts title attributes to data-tooltip, injects tooltip CSS styles, and sets
 * up a MutationObserver on the torrents tbody to handle dynamically added rows.
 */
(() => {

  /**
   * @function convertTooltip
   * @description Finds all elements matching the selector that have a title attribute,
   * converts the title to a data-tooltip attribute (reformatting bullet-separated content),
   * removes the native title, and adds the "barTooltip" class for CSS styling.
   * @param {string} selector - CSS selector to find elements with title attributes.
   * @returns {void}
   */
  function convertTooltip(selector) {
    const elements = document.querySelectorAll(selector);
    if (!elements.length) {return;}
    elements.forEach((element) => {
      let tooltipContent = element.getAttribute("title");
      tooltipContent = tooltipContent.replace(/^(.*)\s•\s(.*)$/, '• $1\n• $2');
      element.setAttribute("data-tooltip", tooltipContent);
      element.removeAttribute("title");
      element.classList.add("barTooltip");
    });
  }

  const styleElement = document.createElement("style");
  styleElement.innerHTML =
    '.barTooltip{position:relative}' +
    '.barTooltip:hover,#snarkTbody:hover .barTooltip{overflow:visible}' +
    '.barTooltip::before,.barTooltip::after{position:absolute;left:50%;opacity:0;box-shadow:2px 2px 2px #0004;pointer-events:none;transition:ease .2s opacity .2s}' +
    '.barTooltip::before{padding:5px 10px;position:absolute;bottom:120%;z-index:999;line-height:1.3;white-space:pre;font-size:90%;font-weight:500;color:var(--ink);border-radius:4px;background:var(--tooltip);transform:translateX(-50%);content:attr(data-tooltip)}' +
    '.barTooltip::after{bottom:calc(120% - 6px);z-index:999;border:6px solid var(--tooltip);content:"";transform:translateX(-50%) rotate(45deg)}'+
    '.txd .barTooltip::before{bottom:calc(128%)}' +
    '.txd .barTooltip::after{bottom:calc(128% - 6px)}' +
    '.barTooltip:hover::before,.barTooltip:hover::after,.txd:hover .barTooltip::before,.txd:hover .barTooltip::after{opacity:1}';
  document.head.appendChild(styleElement);

  convertTooltip(".tx[title], .barComplete[title]");

  let convertPending = false;

  /**
   * @function queueConvert
   * @description Schedules a single tooltip conversion per animation frame, coalescing
   * the observer's per-record callbacks so a morphdom row update triggers one table
   * scan instead of one per mutation.
   * @returns {void}
   */
  function queueConvert() {
    if (convertPending) {return;}
    convertPending = true;
    requestAnimationFrame(() => {
      convertPending = false;
      convertTooltip(".tx[title], .barComplete[title]");
    });
  }

  const observer = new MutationObserver(() => {queueConvert();});

  const targetNode = document.querySelector('#torrents tbody');
  if (targetNode) {
    observer.observe(targetNode, { childList: true, subtree: true });
  }
})();