UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

186 lines (185 loc) 7.58 kB
import { escapeHtml } from '../../shared/escape-html.js'; export function renderArrayModalMarkup(initialTitle) { return ` <div class="array-modal" id="array-modal" hidden> <div class="array-modal-card"> <header> <h3 id="array-modal-title">${escapeHtml(initialTitle)}</h3> <div class="array-modal-actions"> <button type="button" id="array-modal-save" aria-label="Save list changes"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 13l4 4L19 7"></path></svg> </button> <button type="button" id="array-modal-close" aria-label="Close list editor"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7L7 17"></path></svg> </button> </div> </header> <p class="array-modal-description" id="array-modal-description"></p> <p class="array-modal-hint">Type an item, then press Enter (or click away) to add it. A new empty row appears automatically.</p> <p class="array-modal-error hidden" id="array-modal-error" role="status" aria-live="polite"></p> <div class="array-modal-list" id="array-modal-list"></div> </div> </div> `; } export function createArrayModalController(options) { const { container, parseEntryItems, formatEntryTitle, onSave } = options; const modal = container.querySelector('#array-modal'); const modalList = container.querySelector('#array-modal-list'); const modalTitleElement = container.querySelector('#array-modal-title'); const modalDescriptionElement = container.querySelector('#array-modal-description'); const modalErrorElement = container.querySelector('#array-modal-error'); const modalClose = container.querySelector('#array-modal-close'); const modalSave = container.querySelector('#array-modal-save'); let modalEntryKey = null; let modalItems = []; const clearError = () => { if (!modalErrorElement) { return; } modalErrorElement.textContent = ''; modalErrorElement.classList.add('hidden'); }; const showError = (message) => { if (!modalErrorElement) { return; } modalErrorElement.textContent = message; modalErrorElement.classList.remove('hidden'); }; const collectModalItemsFromDom = () => { if (!modalList) { return [...modalItems]; } const existing = Array.from(modalList.querySelectorAll('.array-modal-item-input')) .map((input) => input.value.trim()) .filter((value) => value.length > 0); const newInput = modalList.querySelector('#array-modal-new-item'); const newValue = newInput?.value.trim() ?? ''; if (newValue.length > 0) { return [newValue, ...existing]; } return existing; }; const renderModalList = () => { if (!modalList) { return; } modalList.innerHTML = ` <div class="array-modal-row array-modal-row--new"> <input id="array-modal-new-item" type="text" placeholder="Add item" aria-label="Add new item" /> </div> ${modalItems.map((item, index) => ` <div class="array-modal-row" data-modal-index="${index}"> <input class="array-modal-item-input" type="text" value="${escapeHtml(item)}" aria-label="Item ${index + 1}" /> <button type="button" class="array-modal-item-remove" aria-label="Remove item ${index + 1}"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 10v7M14 10v7"></path></svg> </button> </div> `).join('')} `; const addModalItem = (value) => { const normalized = value.trim(); if (!normalized) { return; } const exists = modalItems.some((item) => item.trim() === normalized); if (!exists) { modalItems.unshift(normalized); } renderModalList(); }; const newInput = modalList.querySelector('#array-modal-new-item'); newInput?.addEventListener('blur', () => { const value = newInput.value.trim(); if (!value) { return; } addModalItem(value); const refreshedNewInput = modalList.querySelector('#array-modal-new-item'); refreshedNewInput?.focus(); }); newInput?.addEventListener('keydown', (event) => { if (event.key !== 'Enter') { return; } event.preventDefault(); const value = newInput.value.trim(); if (!value) { return; } addModalItem(value); const refreshedNewInput = modalList.querySelector('#array-modal-new-item'); refreshedNewInput?.focus(); }); modalList.querySelectorAll('.array-modal-row[data-modal-index]').forEach((rowElement) => { const row = rowElement; const index = Number(row.dataset.modalIndex); const input = row.querySelector('.array-modal-item-input'); const removeButton = row.querySelector('.array-modal-item-remove'); input?.addEventListener('blur', () => { modalItems[index] = input.value.trim(); modalItems = modalItems.filter((item) => item.length > 0); renderModalList(); }); removeButton?.addEventListener('click', () => { modalItems = modalItems.filter((_, itemIndex) => itemIndex !== index); renderModalList(); }); }); }; const close = () => { if (modal) { modal.hidden = true; } modalEntryKey = null; clearError(); }; const open = (entry) => { modalEntryKey = entry.key; modalItems = parseEntryItems(entry); clearError(); if (modalTitleElement) { modalTitleElement.textContent = formatEntryTitle(entry); } if (modalDescriptionElement) { modalDescriptionElement.textContent = entry.description ?? ''; modalDescriptionElement.classList.toggle('hidden', !modalDescriptionElement.textContent.trim()); } renderModalList(); if (modal) { modal.hidden = false; } }; modalClose?.addEventListener('click', close); modal?.addEventListener('click', (event) => { if (event.target === modal) { close(); } }); modalSave?.addEventListener('click', async () => { if (!modalEntryKey) { return; } const changedKey = modalEntryKey; modalItems = collectModalItemsFromDom(); clearError(); if (modalSave) { modalSave.disabled = true; } try { await onSave(changedKey, modalItems); close(); } catch (error) { const message = error instanceof Error ? error.message : String(error); showError(message.trim().length > 0 ? message : 'Failed to save list changes.'); } finally { if (modalSave) { modalSave.disabled = false; } } }); return { open, close }; }