import { getItemKey, isPageScopeItem } from "./content-edit-target.js"; const DEBOUNCE_MS = 400; function itemLabel(item) { const instruction = item.instruction || ""; const text = instruction.length > 80 ? `${instruction.slice(0, 80)}…` : instruction; if (isPageScopeItem(item)) { return text ? `Страница: ${text}` : "Общая правка страницы"; } return text; } export function createDock({ onSave, onSelect, onDraft, onDelete }) { const root = document.createElement("div"); root.className = "content-edit-dock glass-neutral"; root.setAttribute("data-content-edit-ui", ""); root.hidden = true; root.innerHTML = `

Список правок

Инструкция агенту

Shift+клик по блоку — вставить ссылку в текст

`; const listEl = root.querySelector(".content-edit-dock__items"); const emptyEl = root.querySelector(".content-edit-dock__empty"); const textarea = root.querySelector("textarea"); let debounceTimer = null; let currentItem = null; let suppressInput = false; let items = []; let selectedKey = null; let resizeObserver = null; let savedCaret = null; function captureCaret() { if (textarea.disabled) return; savedCaret = { start: textarea.selectionStart, end: textarea.selectionEnd, }; } function syncDockHeight() { const h = root.getBoundingClientRect().height; document.documentElement.style.setProperty("--content-edit-dock-height", `${Math.ceil(h)}px`); } function observeHeight() { if (resizeObserver) return; resizeObserver = new ResizeObserver(syncDockHeight); resizeObserver.observe(root); } function unobserveHeight() { resizeObserver?.disconnect(); resizeObserver = null; document.documentElement.style.removeProperty("--content-edit-dock-height"); } function scrollSelectedIntoList() { const btn = listEl.querySelector(".content-edit-dock__item.is-selected"); btn?.scrollIntoView({ block: "nearest", behavior: "smooth" }); } function scheduleSave() { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { if (!currentItem) return; onSave({ item: currentItem, instruction: textarea.value }); }, DEBOUNCE_MS); } function patchCurrentInstruction(raw, { draft = true } = {}) { if (!currentItem) return; currentItem = { ...currentItem, instruction: raw }; if (draft) onDraft?.({ item: currentItem, instruction: raw }); } function patchCurrentItem(fields) { if (!currentItem) return; currentItem = { ...currentItem, ...fields }; const instruction = fields.instruction ?? currentItem.instruction ?? textarea.value; currentItem = { ...currentItem, instruction }; onDraft?.({ item: currentItem, instruction }); } textarea.addEventListener("input", () => { if (suppressInput) return; patchCurrentInstruction(textarea.value); captureCaret(); scheduleSave(); }); textarea.addEventListener("keyup", captureCaret); textarea.addEventListener("click", captureCaret); textarea.addEventListener("select", captureCaret); function renderList() { listEl.replaceChildren(); if (!items.length) { emptyEl.hidden = false; return; } emptyEl.hidden = true; for (const item of items) { const li = document.createElement("li"); li.className = "content-edit-dock__row"; const btn = document.createElement("button"); btn.type = "button"; btn.className = "content-edit-dock__item"; btn.setAttribute("role", "option"); btn.dataset.itemKey = getItemKey(item); btn.textContent = `${item.n}. ${itemLabel(item)}`; const itemKey = btn.dataset.itemKey; if (itemKey && itemKey === selectedKey) { btn.classList.add("is-selected"); btn.setAttribute("aria-selected", "true"); } else { btn.setAttribute("aria-selected", "false"); } btn.addEventListener("click", () => onSelect?.(item)); const deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "content-edit-dock__delete"; deleteBtn.title = "Удалить"; deleteBtn.setAttribute("aria-label", `Удалить правку ${item.n}`); deleteBtn.innerHTML = ''; deleteBtn.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); onDelete?.(item); }); li.appendChild(btn); li.appendChild(deleteBtn); listEl.appendChild(li); } if (selectedKey && items.some((i) => getItemKey(i) === selectedKey)) { requestAnimationFrame(scrollSelectedIntoList); } } function itemKeyOf(item) { return getItemKey(item) || null; } function loadEditor(item) { currentItem = item; selectedKey = itemKeyOf(item); suppressInput = true; textarea.value = item?.instruction ?? ""; textarea.disabled = !item; textarea.placeholder = isPageScopeItem(item) ? "Общая правка страницы: что изменить на странице…" : "Что изменить, добавить после, куда перенести…"; suppressInput = false; renderList(); } return { el: root, textarea, open() { root.hidden = false; observeHeight(); requestAnimationFrame(syncDockHeight); }, close() { root.hidden = true; unobserveHeight(); currentItem = null; selectedKey = null; clearTimeout(debounceTimer); }, isOpen() { return !root.hidden; }, setItems(nextItems) { items = nextItems; if (currentItem) { const key = itemKeyOf(currentItem); const fresh = items.find((i) => itemKeyOf(i) === key); if (fresh) { currentItem = { ...fresh, instruction: textarea.disabled ? fresh.instruction : textarea.value, }; } } renderList(); }, selectItem(item, { focusEditor = true } = {}) { loadEditor(item); captureCaret(); if (focusEditor && item) textarea.focus(); }, clearSelection() { currentItem = null; selectedKey = null; suppressInput = true; textarea.value = ""; textarea.disabled = true; suppressInput = false; renderList(); }, getCurrentItem() { return currentItem; }, flushSave() { clearTimeout(debounceTimer); if (!currentItem) return; onSave({ item: currentItem, instruction: textarea.value }); }, insertAtCaret(text, insertContext = null) { if (!currentItem || textarea.disabled || !text) return false; const baseText = insertContext?.text ?? textarea.value; let start; let end; if (insertContext?.caret) { ({ start, end } = insertContext.caret); } else if (document.activeElement === textarea) { start = textarea.selectionStart; end = textarea.selectionEnd; } else if (savedCaret) { ({ start, end } = savedCaret); } else { start = baseText.length; end = baseText.length; } const before = baseText.slice(0, start); const after = baseText.slice(end); const next = before + text + after; suppressInput = true; textarea.value = next; const pos = start + text.length; textarea.setSelectionRange(pos, pos); suppressInput = false; savedCaret = { start: pos, end: pos }; patchCurrentInstruction(next, { draft: false }); scheduleSave(); textarea.focus(); return true; }, getInsertContext() { if (!currentItem || textarea.disabled) return null; const text = textarea.value; let caret; if (document.activeElement === textarea) { caret = { start: textarea.selectionStart, end: textarea.selectionEnd, }; } else if (savedCaret) { caret = { ...savedCaret }; } else { caret = { start: text.length, end: text.length }; } return { text, caret }; }, saveCaret: captureCaret, patchItem(fields) { patchCurrentItem(fields); }, }; }