Initial commit
Photo-based book cataloger with AI identification. Room → Cabinet → Shelf → Book hierarchy; FastAPI + SQLite backend; vanilla JS SPA; OpenAI-compatible plugin system for boundary detection, text recognition, and archive search.
This commit is contained in:
65
static/js/editing.js
Normal file
65
static/js/editing.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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()
|
||||
*/
|
||||
|
||||
// ── 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(_){} });
|
||||
_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(); }
|
||||
},
|
||||
}));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user