const expandedNodeIds = new Set(); document.addEventListener('DOMContentLoaded', () => { const textId = window.location.pathname.split('/').pop(); fetch('/api/texts/' + textId, { credentials: 'same-origin' }) .then(function (res) { if (!res.ok) { if (res.status === 403) { const message = document.createElement('p'); message.textContent = "You don't have permission to view this text"; document.getElementById('text-detail').appendChild(message); } else if (res.status === 404) { const message = document.createElement('p'); message.textContent = 'Text not found'; document.getElementById('text-detail').appendChild(message); } return; } return res.json(); }) .then(function (text) { if (!text) return; const h1 = document.createElement('h1'); h1.textContent = text.name; document.getElementById('text-detail').appendChild(h1); return fetchAndRenderNodes(textId); }); }); function fetchAndRenderNodes(textId) { return fetch('/api/nodes/' + textId, { credentials: 'same-origin' }) .then(res => res.json()) .then(nodes => { const existing = document.querySelector('#text-detail > ul'); if (existing) existing.remove(); const tree = buildTree(nodes); const ul = renderTree(tree, textId); document.getElementById('text-detail').appendChild(ul); }); } function buildTree(nodes) { const map = {}; nodes.forEach(node => { map[node.id] = { ...node, children: [] }; }); const roots = []; nodes.forEach(node => { if (node.parentNodeId === null) { roots.push(map[node.id]); } else if (map[node.parentNodeId]) { map[node.parentNodeId].children.push(map[node.id]); } }); return roots; } function renderTree(nodes, textId, depth = 0) { const ul = document.createElement('ul'); nodes.forEach(node => { const li = document.createElement('li'); const titleSpan = document.createElement('span'); titleSpan.textContent = node.title; li.appendChild(titleSpan); const addBtn = document.createElement('button'); addBtn.textContent = 'Add child'; addBtn.className = 'add-child'; addBtn.addEventListener('click', () => toggleAddForm(li, node.id, textId)); li.appendChild(addBtn); const bulkBtn = document.createElement('button'); bulkBtn.textContent = 'Bulk add children'; bulkBtn.className = 'bulk-add-children'; bulkBtn.addEventListener( 'click', () => toggleBulkAddForm(li, node.id, textId) ); li.appendChild(bulkBtn); if (node.children.length > 0) { const childUl = renderTree(node.children, textId, depth + 1); const childrenVisible = expandedNodeIds.has(node.id) || depth === 0; if (childrenVisible) { expandedNodeIds.add(node.id); } childUl.style.display = childrenVisible ? '' : 'none'; const toggleBtn = document.createElement('button'); toggleBtn.className = 'toggle-children'; toggleBtn.textContent = childrenVisible ? '▼' : '▶'; toggleBtn.addEventListener('click', () => { const isHidden = childUl.style.display === 'none'; childUl.style.display = isHidden ? '' : 'none'; toggleBtn.textContent = isHidden ? '▼' : '▶'; if (isHidden) { expandedNodeIds.add(node.id); } else { expandedNodeIds.delete(node.id); } }); li.appendChild(toggleBtn); li.appendChild(childUl); } ul.appendChild(li); }); return ul; } function closeAllAddForms() { const selectors = [ 'input.child-title', 'button.save-child', 'input.bulk-title', 'input.bulk-count', 'button.save-bulk', ]; selectors.forEach((selector) => { document .querySelectorAll('#text-detail ' + selector) .forEach((element) => element.remove()); }); } function toggleAddForm(li, parentNodeId, textId) { const existing = li.querySelector('input.child-title'); if (existing) { existing.remove(); li.querySelector('button.save-child').remove(); return; } closeAllAddForms(); const input = document.createElement('input'); input.type = 'text'; input.className = 'child-title'; input.placeholder = 'Node title'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.className = 'save-child'; function submit() { const title = input.value.trim(); if (!title) return; expandedNodeIds.add(parentNodeId); fetch('/api/nodes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ textId: parseInt(textId), title, parentNodeId }), }) .then(res => { if (!res.ok) throw new Error('Failed to create node'); return res.json(); }) .then(() => fetchAndRenderNodes(textId)); } saveBtn.addEventListener('click', submit); input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); submit(); } }); li.appendChild(input); li.appendChild(saveBtn); input.focus(); } function toggleBulkAddForm(li, parentNodeId, textId) { const existing = li.querySelector('input.bulk-title'); if (existing) { existing.remove(); li.querySelector('input.bulk-count').remove(); li.querySelector('button.save-bulk').remove(); return; } closeAllAddForms(); const titleInput = document.createElement('input'); titleInput.type = 'text'; titleInput.className = 'bulk-title'; titleInput.placeholder = 'Title prefix'; const countInput = document.createElement('input'); countInput.type = 'number'; countInput.className = 'bulk-count'; countInput.placeholder = 'Count'; countInput.min = '1'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.className = 'save-bulk'; function submit() { const titlePrefix = titleInput.value.trim(); const count = parseInt(countInput.value); if (!titlePrefix || !count || count < 1) return; expandedNodeIds.add(parentNodeId); fetch('/api/nodes/bulk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ textId: parseInt(textId), parentNodeId, titlePrefix, count }), }) .then(res => { if (!res.ok) throw new Error('Failed to bulk create nodes'); return res.json(); }) .then(() => fetchAndRenderNodes(textId)); } function submitOnEnter(event) { if (event.key === 'Enter') { event.preventDefault(); submit(); } } saveBtn.addEventListener('click', submit); titleInput.addEventListener('keydown', submitOnEnter); countInput.addEventListener('keydown', submitOnEnter); li.appendChild(titleInput); li.appendChild(countInput); li.appendChild(saveBtn); titleInput.focus(); }