extract the save-child handler into a submit closure shared by the save button click and a keydown listener on the input. also focus the input as soon as the form opens so the user can type and hit enter without touching the mouse.
197 lines
6.3 KiB
JavaScript
197 lines
6.3 KiB
JavaScript
const expandedNodeIds = new Set();
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const textId = window.location.pathname.split('/').pop();
|
|
|
|
fetch('/api/texts/' + textId, { credentials: 'same-origin' })
|
|
.then(res => res.json())
|
|
.then(text => {
|
|
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 toggleAddForm(li, parentNodeId, textId) {
|
|
const existing = li.querySelector('input.child-title');
|
|
if (existing) {
|
|
existing.remove();
|
|
li.querySelector('button.save-child').remove();
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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';
|
|
saveBtn.addEventListener('click', () => {
|
|
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));
|
|
});
|
|
|
|
li.appendChild(titleInput);
|
|
li.appendChild(countInput);
|
|
li.appendChild(saveBtn);
|
|
}
|