/* * editing.js * Inline contenteditable name editing for tree nodes (blur-to-save, strips * leading emoji prefix) and SortableJS drag-and-drop reorder wiring. * * SortableJS is loaded as an external CDN script (must precede this file). * _sortables is managed entirely within this module; render() in init.js * only needs to call initSortables() to refresh after a full re-render. * * Depends on: S (state.js); req, toast (api.js / helpers.js); * walkTree (tree-render.js); Sortable (CDN global) * Provides: attachEditables(), initSortables() */ /* exported attachEditables, initSortables */ // ── SortableJS instances (destroyed and recreated on each render) ───────────── let _sortables = []; // ── Inline name editing ────────────────────────────────────────────────────── function attachEditables() { document.querySelectorAll('[contenteditable=true]').forEach((el) => { el.dataset.orig = el.textContent.trim(); el.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); el.blur(); } if (e.key === 'Escape') { el.textContent = el.dataset.orig; el.blur(); } e.stopPropagation(); }); el.addEventListener('blur', async () => { const val = el.textContent.trim(); if (!val || val === el.dataset.orig) { if (!val) el.textContent = el.dataset.orig; return; } const newName = val.replace(/^[🏠📚]\s*/u, '').trim(); const { type, id } = el.dataset; const url = { room: `/api/rooms/${id}`, cabinet: `/api/cabinets/${id}`, shelf: `/api/shelves/${id}` }[type]; if (!url) return; try { await req('PUT', url, { name: newName }); el.dataset.orig = el.textContent.trim(); walkTree((n) => { if (n.id === id) n.name = newName; }); // Update sidebar label if editing from header (sidebar has non-editable nname spans) const sideLabel = document.querySelector(`.node[data-id="${id}"] .nname`); if (sideLabel && sideLabel !== el) { const prefix = type === 'room' ? '🏠 ' : type === 'cabinet' ? '📚 ' : ''; sideLabel.textContent = prefix + newName; } } catch (err) { el.textContent = el.dataset.orig; toast('Rename failed: ' + err.message); } }); el.addEventListener('click', (e) => e.stopPropagation()); }); } // ── SortableJS drag-and-drop ───────────────────────────────────────────────── function initSortables() { _sortables.forEach((s) => { try { s.destroy(); } catch (_) { // ignore destroy errors on stale instances } }); _sortables = []; document.querySelectorAll('.sortable-list').forEach((el) => { const type = el.dataset.type; _sortables.push( Sortable.create(el, { handle: '.drag-h', animation: 120, ghostClass: 'drag-ghost', onEnd: async () => { const ids = [...el.querySelectorAll(':scope > .node')].map((n) => n.dataset.id); try { await req('PATCH', `/api/${type}/reorder`, { ids }); } catch (_err) { toast('Reorder failed'); await loadTree(); } }, }), ); }); }