extract the save-bulk handler into a submit closure shared by the save button click and a keydown listener on both the title and count inputs. focus the title input as soon as the form opens.
210 lines
6.6 KiB
JavaScript
210 lines
6.6 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';
|
|
|
|
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();
|
|
}
|