import {
  select_default
} from "./chunk-I6Y4O3RR.js";

// src/app.observer.js
var clickableSjClasses = [
  "sja_menuoption",
  "sja_clb",
  "sja_clbtext",
  "sja_clbtext2",
  "sjpp-dslabel",
  "sjpp-active-tiny-button",
  "sja_inset_a",
  "sjpp-matrix-label",
  "sjpp-matrix-divide-by-label",
  "sjpp-matrix-legend-g"
];
var svgClickable = /* @__PURE__ */ new Set([
  /*'text' /*, 'rect'*/
]);
var targetTagNames = /* @__PURE__ */ new Set(["DIV", "SPAN", "TEXT", "LABEL", "g", ...[...svgClickable]]);
var renderWait = 500;
var observedElems = /* @__PURE__ */ new WeakSet();
function observeElem(elem) {
  if (window.location.pathname === "/puppet.html") return;
  if (observedElems.has(elem)) return;
  observedElems.add(elem);
  observer.observe(elem, { childList: true, subtree: true });
}
var observer = new MutationObserver(function(mutationsList, observer2) {
  const relevantElems = /* @__PURE__ */ new Set();
  for (const mutation of mutationsList) {
    for (const elem of mutation.addedNodes) {
      if (elem.nodeType === Node.ELEMENT_NODE) {
        if (!targetTagNames.has(elem.tagName) || elem.tabIndex !== -1) continue;
        relevantElems.add(elem);
      }
    }
  }
  if (!relevantElems.size) return;
  setTimeout(() => {
    for (const elem of relevantElems) {
      if (elem.getAttribute("tabindex") !== null) continue;
      const selem = select_default(elem);
      if (!selem.on("click") && !svgClickable.has(elem.tagName) && !clickableSjClasses.find((cls) => elem.classList?.contains(cls)))
        continue;
      selem.attr("tabindex", 0).on("keyup.root_observer", (event2) => {
        if (event2.key == "Enter") {
          const box = event2.target.getBoundingClientRect();
          const clientX = box.x + 0.5 * box.width;
          const clientY = box.y + box.height;
          event2.target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX, clientY }));
          event2.target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX, clientY }));
          event2.target.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX, clientY }));
        }
      });
    }
    relevantElems.clear();
  }, renderWait);
});

// common/globals.js
var base_zindex = 100;
function get_base_zindex() {
  return base_zindex;
}
function set_base_zindex(i) {
  base_zindex = i;
}

// dom/menu.js
var focusableSelector = `a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]), ` + clickableSjClasses.map((c) => `.${c}`).join(",");
var showMenuTriggers = /* @__PURE__ */ new Set([
  /*'mousemove',*/
  "mousedown",
  "mouseup",
  "click"
]);
var urlpath = window.location.pathname;
var isTestPage = urlpath.includes("testrun.html") || urlpath.includes("puppet.html");
var Menu = class {
  constructor(arg = {}) {
    this.typename = Math.random().toString();
    const body = select_default(document.body);
    this.d = body.append("div").attr("class", "sja_menu_div").attr("data-testid", arg.testid || "sjpp-menu-div").style("display", "none").style("position", "absolute").style("background-color", "white").on("mousedown.menu" + this.typename, (event2) => {
      const t = select_default(event2.target);
      if (
        /*** 
        	NOTE on interactive menu elements: 
        	Any menu element (input, button, etc) that has event listeners
          is expected to handle that event, including possibly closing any
          associated menus, and thus no need to propagate the mousedown event 
        	to the document body
        ***/
        t.on("mousedown") || t.on("click") || // also assume that the following elements have event listeners by default,
        // so same logic of not wanting to propagate the event to the body
        ["INPUT", "SELECT", "TEXTAREA"].includes(event2.target.tagName.toUpperCase())
      ) {
        event2.stopPropagation();
      }
    }).on("keyup", (event2) => {
      if (event2.key == "Escape") this.hide();
    });
    this.dnode = this.d.node();
    this.setRelatedMenus(arg);
    body.on("mousedown.menu" + this.typename, (event2) => {
      if (this.dnode.contains(event2.target)) return;
      if (event2.target.parent_menu === this.dnode) return;
      if (this.dnode.ancestor_menus?.includes(this.dnode)) return;
      const menu = event2.target.closest(".sja_menu_div");
      if (menu && (menu.parent_menu === this.dnode || menu.ancestor_menus?.includes(this.dnode))) return;
      this.hide();
    });
    const base_zindex2 = get_base_zindex();
    if (base_zindex2) {
      this.d.style("z-index", base_zindex2 + 1);
    }
    this.d.style("padding", "padding" in arg ? arg.padding : "10px");
    if (arg.border) {
      this.d.style("border", arg.border);
    } else {
      this.d.style("box-shadow", "0px 2px 4px 1px #999");
    }
    this.offsetX = Number.isInteger(arg.offsetX) ? arg.offsetX : 20;
    this.offsetY = Number.isInteger(arg.offsetY) ? arg.offsetY : 20;
    this.hideXmute = Number.isInteger(arg.hideXmute) ? arg.hideXmute : 0;
    this.hideYmute = Number.isInteger(arg.hideYmute) ? arg.hideYmute : 0;
    this.prevX = -1;
    this.prevY = -1;
    this.clearSelector = arg.clearSelector;
    if (arg.onHide) this.onHide = arg.onHide;
  }
  setRelatedMenus(arg) {
    if (Object.values(this.d._groups[0]).length) this.dnode.parent_menu = arg.parent_menu;
    this.dnode.ancestor_menus = arg.ancestor_menus || [];
    if (this.dnode.parent_menu && !this.dnode.ancestor_menus.includes(this.dnode.parent_menu)) {
      this.dnode.ancestor_menus.push(this.dnode.parent_menu);
    }
    const ancestor_menus = this.dnode.ancestor_menus.slice(0);
    while (ancestor_menus.length) {
      const m = ancestor_menus.pop();
      if (!m) continue;
      if (m.parent_menu && !this.dnode.ancestor_menus.includes(m.parent_menu)) {
        this.dnode.ancestor_menus.push(m.parent_menu);
      }
      if (m.ancestor_menus) {
        for (const mm of m.ancestor_menus) {
          if (!this.dnode.ancestor_menus.includes(mm)) this.dnode.ancestor_menus.push(mm);
        }
      }
    }
  }
  clear() {
    if (this.clearSelector) {
      const elem = this.d.select(this.clearSelector);
      elem.selectAll("*").remove();
      elem.node().replaceChildren();
    } else {
      this.d.selectAll("*").remove();
      this.dnode.replaceChildren();
    }
    return this;
  }
  /*
   To note: if shift and scroll are true, shift sets x & y.
   Set shift to false to use scroll
    
    _elem: the element for which the menu was launched, optional
  */
  show(_x, _y, shift = true, down = true, scroll = true, elem = null) {
    let x = _x;
    let y = _y;
    this.prevX = _x;
    this.prevY = _y;
    document.body.appendChild(this.dnode);
    this.d.style("display", "block");
    if (scroll) {
      x = x + window.scrollX;
      y = y + window.scrollY;
    }
    if (shift) {
      x = x + this.offsetX;
      y = y + this.offsetY;
    }
    const width = window.innerWidth;
    const height = window.innerHeight;
    const middlex = width / 2;
    const middley = height / 2;
    const p = this.dnode.getBoundingClientRect();
    if (width - x < middlex && x + p.width > width) this.d.style("left", null).style("right", width - x + "px");
    else this.d.style("left", x + "px").style("right", null);
    if (!down && height - y < middley && y - window.scrollY > p.height) {
      this.d.style("top", `${y - p.height}px`).style("bottom", null);
    } else this.d.style("top", y + "px").style("bottom", null);
    this.d.transition().style("opacity", 1);
    if (elem) this.setTabNavigation(elem);
    else if (showMenuTriggers.has(window.event?.type)) this.setTabNavigation(window.event.target);
    if (elem) this.stickyPosition(elem);
    return this;
  }
  // simplified alternative to show(), with less arguments
  // _x, _y: desired position in the client window
  // opts{}:
  //   elem: the element for which the menu was launched, will get focus when tabbing back from the menu
  show2(_x, _y, opts = {}) {
    let x = _x;
    let y = _y;
    this.prevX = _x;
    this.prevY = _y;
    document.body.appendChild(this.dnode);
    this.d.style("display", "block");
    x = x + window.scrollX + this.offsetX;
    y = y + window.scrollY + this.offsetY;
    const width = window.innerWidth;
    const height = window.innerHeight;
    const middlex = width / 2;
    const middley = height / 2;
    const p = this.dnode.getBoundingClientRect();
    if (x + p.width > width) this.d.style("left", "").style("right", "5px");
    else this.d.style("left", x + "px").style("right", "");
    this.d.style("top", y + "px").style("bottom", "");
    this.d.transition().style("opacity", 1);
    if (opts.elem) this.setTabNavigation(opts.elem);
    return this;
  }
  // opts{}: can supply offsetX and offsetY
  // necessary because .showunder() uses .show(shift=false), which
  // ignores offsetY in the constructor option
  showunder(elem, _opts = {}) {
    const opts = Object.assign({ offsetX: 0, offsetY: 0 }, _opts);
    const p = elem.getBoundingClientRect();
    const x = p.left + window.scrollX + opts.offsetX;
    const y = p.top + p.height + window.scrollY + 5 + opts.offsetY;
    return this.show(x, y, false, true, false, elem);
  }
  async setTabNavigation(elem, numTries = 0) {
    const clickableElems = [...this.d.node().querySelectorAll(focusableSelector)].filter((elem2) => elem2.checkVisibility());
    if (!clickableElems.length) {
      if (numTries < 30) setTimeout(() => this.setTabNavigation(elem, numTries++), 100);
      return;
    }
    const firstFocusableChildElem = select_default(clickableElems[0]);
    for (const elem2 of clickableElems) {
      const s = select_default(elem2);
      if (!elem2.getAttribute("tabindex")) s.attr("tabindex", 0);
      if (!s.on("keyup"))
        setTimeout(() => {
          s.on("keyup.menu_tab_nav", (event2) => {
            if (event2.key == "Enter") elem2.click();
          });
        }, renderWait);
    }
    let lastTabbedTime = Date.now();
    function setLastTabbedTime(event2) {
      if (event2.key == "Tab" && event2.shiftKey) lastTabbedTime = Date.now();
    }
    function focusOnLauncherElem(tip, launcherElem) {
      if (Date.now() - lastTabbedTime > tabWait) return;
      if (document.activeElement === document.body || tip.dnode.contains(document.activeElement) || event.target?.closest(".sja_menu_div")?.ancestor_menus?.includes(this.dnode))
        return;
      tip.hide();
      launcherElem.focus();
    }
    const tabWait = 100;
    firstFocusableChildElem.on("keydown.menu_tab_nav", setLastTabbedTime).on("blur.menu_tab_nav", () => focusOnLauncherElem(this, elem));
    const searchElem = elem.type === "search" ? elem : elem.querySelector(`input[type='search']`);
    if (!searchElem) {
      setTimeout(() => (clickableElems.find(priorityFocusElem) || clickableElems[0]).focus(), renderWait);
    } else if (searchElem) {
      const search = select_default(searchElem);
      if (!search.on("keyup")) {
        search.on("keyup", (event2) => {
          if (!event2.key.startsWith("Arrow")) return;
          const clickableElems2 = [...this.d.node().querySelectorAll(focusableSelector)].filter(
            (elem2) => elem2.checkVisibility()
          );
          if (!clickableElems2.length) return;
          if (event2.key == "ArrowDown") {
            clickableElems2[0].focus();
            select_default(clickableElems2[0]).on("keydown.menu_tab_nav", setLastTabbedTime).on("blur.menu_tab_nav", () => focusOnLauncherElem(this, searchElem));
          }
        });
      }
    }
  }
  showunderoffset(dom) {
    const p = dom.getBoundingClientRect();
    const y = p.top + p.height + window.scrollY + 5;
    return this.show(p.left, y, true, true, false);
  }
  // stickyPosition() prevents the menu from being scrolled away (above or below) the clicked element,
  // an issue seen when the clicked element is nested inside a sticky-positioned div such as
  // the MASS nav header, whereas the menu div that's attached to the body will scroll freely past the
  // 'stuck' clicked element
  stickyPosition(elem, _opts = {}) {
    if (!window.event || window.event.type != "click" || !elem.__data__?.stickyAncestor) return;
    const top = this.d.style("top");
    if (!top.endsWith("px")) return;
    const { height: elemHeight } = elem.getBoundingClientRect();
    const scrollStartY = window.scrollY;
    const yPos = Number(top.replace("px", ""));
    let startRatio;
    const observerOptions = {
      // Use a negative margin equal to the 'top' CSS value to trigger
      // the callback when the element reaches its sticky position.
      rootMargin: "-0.1px 0px 0px 0px",
      // Use a small negative value like -0.1px or -1px
      threshold: [
        /*0.7, 0.8,*/
        0.95,
        1
      ]
      // Triggers when the target is xx% visible, when it starts getting stuck or moving again
    };
    this.stickyObserver = new IntersectionObserver(([entry]) => {
      if (startRatio === void 0) startRatio = entry.intersectionRatio;
      if (entry.intersectionRatio >= 1) {
        const yAdjust = startRatio >= 1 ? 0 : window.scrollY - scrollStartY + entry.intersectionRect.y;
        this.d.style("top", `${yPos + yAdjust}px`).style("position", "absolute");
      } else if (entry.intersectionRatio < 0.95) {
        if (startRatio < 0.95) return;
        this.d.style("top", `${yPos + window.scrollY - scrollStartY - entry.intersectionRect.height}px`).style("position", "absolute");
      } else {
        this.d.style("top", `${elem.getBoundingClientRect().y + elemHeight}px`).style("position", "fixed");
      }
    }, observerOptions);
    this.stickyObserver.observe(elem.__data__.stickyAncestor);
  }
  // this hide() method may be overriden with a custom method by getCustomApi(overrides)
  hide(event2) {
    if (event2) {
      if (Math.abs(this.prevX - event2.clientX) < this.hideXmute && Math.abs(this.prevY - event2.clientY) < this.hideYmute)
        return;
    }
    if (!this.fadeTimeout) this.d.style("display", "none").style("opacity", 0);
    if (this.onHide) this.onHide();
    this.stopObserver();
    return this;
  }
  fadeout() {
    this.stopObserver();
    this.d.transition().style("opacity", 0).on("end", () => this.d.style("display", "none"));
    return this;
  }
  confirm({ html, timeout }) {
    this.d.selectAll("*").remove();
    this.d.append("div").style("padding", "5px").html(html);
    if (timeout)
      this.fadeTimeout = setTimeout(() => {
        this.fadeout();
        delete this.fadeTimeout;
      }, 3e3);
  }
  toggle() {
    if (!this.hidden) {
      this.hide();
      this.hidden = true;
    } else {
      this.d.style("opacity", 1).style("display", "block");
      this.hidden = false;
    }
    return this;
  }
  /*
  	Create a custom instance API with method and/or property overrides;
  	this will simplify sharing of the same tip instance among different
  	components/code, such as a chart edit menu with an embedded termsetting
  	edit menu, where the tip.hide() can be shared but tip.clear() will be
  	applied only to a specific div in the overall menu div
  	
  	Example usage: 
  	const tip = new Menu()
  	const subsection = tip.d.append('div')
  	const customTipApi = tip.getCustomApi({
  		d: subsection, // expose only a subsection of the whole tip.d,
  		show: () => {
  			subsection.style('display', 'block')
  		},
  		hide: () => {
  			subsection.style('display', 'none')
  		},
  		clear: () => {
  			subsection.selectAll('*').remove() // clear only the subsection
  			return customTipApi
  		}
  	})
  */
  getCustomApi(overrides = {}) {
    const api = Object.create(this);
    Object.assign(api, overrides);
    return api;
  }
  stopObserver() {
    if (!this.stickyObserver) return;
    this.stickyObserver.disconnect();
    delete this.stickyObserver;
  }
  destroy() {
    this.stopObserver();
    this.d.remove();
  }
};
function priorityFocusElem(elem, i) {
  return elem.tagName == "INPUT" && elem.type == "search";
}
function getAncestorWithComputedStyle(elem, key, values) {
  if (isTestPage || !elem || !values.size) return;
  const style = window.getComputedStyle(elem);
  if (values.has(style[key])) return elem;
  if (elem.tagName == "BODY" || elem.classList?.contains("sja_root_holder") || elem.classList?.contains("sja_menu_div") && elem.parentNode === document.body)
    return;
  return elem.parentNode ? getAncestorWithComputedStyle(elem.parentNode, key, values) : void 0;
}

export {
  set_base_zindex,
  observeElem,
  Menu,
  getAncestorWithComputedStyle
};
//# sourceMappingURL=chunk-HYOEWQ5P.js.map
