Skip to content
quarry Artifact [2f3e10d2d3]
Login

Artifact [2f3e10d2d3]

Artifact 2f3e10d2d33bc90313bd241e3162f51fed4530b6beecd5c4b600d5d9c7b2cec4:


// composer.js — slash commands for quarry's markdown composers.
// First-party, dependency-free. A textarea opts in with data-slash.
// Typing "/" at the start of a line opens a snippet menu; Escape or
// clicking away closes it; without this script the textarea is simply
// a textarea. Snippets insert plain markdown — the raw text stays the
// source of truth.
(function () {
  "use strict";
  var ITEMS = [
    { label: "Heading", hint: "## ", text: "## " },
    { label: "Bold", hint: "**b**", text: "**bold**" },
    { label: "Code block", hint: "```", text: "```\ncode\n```\n" },
    { label: "Quote", hint: "> ", text: "> " },
    { label: "Bullet list", hint: "- ", text: "- " },
    { label: "Task list", hint: "- [ ]", text: "- [ ] " },
    { label: "Details", hint: "fold", text: "<details><summary>summary</summary>\n\nbody\n\n</details>\n" },
    { label: "Table", hint: "| |", text: "| a | b |\n| --- | --- |\n| 1 | 2 |\n" },
    { label: "Poll", hint: "ballot", text: "```poll\nquestion: Your question?\n- option one\n- option two\ncloses: 2026-12-31\n```\n" }
  ];
  var menu = null, target = null, slashAt = -1, sel = 0;

  function close() {
    if (menu) menu.remove();
    menu = null; target = null; slashAt = -1; sel = 0;
  }
  function insert(item) {
    var ta = target, at = slashAt;
    close();
    ta.setRangeText(item.text, at, ta.selectionStart, "end");
    ta.focus();
    ta.dispatchEvent(new Event("input", { bubbles: true }));
  }
  function render() {
    var btns = menu.querySelectorAll("button");
    for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("sel", i === sel);
  }
  function open(ta) {
    close();
    target = ta; slashAt = ta.selectionStart - 1;
    menu = document.createElement("div");
    menu.className = "slash-menu";
    ITEMS.forEach(function (item, i) {
      var b = document.createElement("button");
      b.type = "button";
      b.textContent = item.label;
      var s = document.createElement("small");
      s.textContent = item.hint;
      b.appendChild(s);
      b.addEventListener("mousedown", function (ev) { ev.preventDefault(); insert(item); });
      b.addEventListener("mouseenter", function () { sel = i; render(); });
      menu.appendChild(b);
    });
    var r = ta.getBoundingClientRect();
    menu.style.left = (r.left + window.scrollX + 12) + "px";
    menu.style.top = (r.top + window.scrollY + 28) + "px";
    document.body.appendChild(menu);
    render();
  }
  document.addEventListener("input", function (ev) {
    var ta = ev.target;
    if (!ta.matches || !ta.matches("textarea[data-slash]")) return;
    var p = ta.selectionStart, v = ta.value;
    if (p > 0 && v[p - 1] === "/" && (p === 1 || v[p - 2] === "\n")) { open(ta); return; }
    if (menu) close();
  });
  document.addEventListener("keydown", function (ev) {
    if (!menu) return;
    if (ev.key === "Escape") { close(); return; }
    if (ev.key === "ArrowDown") { ev.preventDefault(); sel = (sel + 1) % ITEMS.length; render(); return; }
    if (ev.key === "ArrowUp") { ev.preventDefault(); sel = (sel + ITEMS.length - 1) % ITEMS.length; render(); return; }
    if (ev.key === "Enter" || ev.key === "Tab") { ev.preventDefault(); insert(ITEMS[sel]); }
  });
  document.addEventListener("click", function (ev) {
    if (menu && !menu.contains(ev.target)) close();
  });
})();