Goal-Calibration/public/js/text.js

174 lines
5.7 KiB
JavaScript

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 = depth === 0;
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 ? '▼' : '▶';
});
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';
saveBtn.addEventListener('click', () => {
const title = input.value.trim();
if (!title) return;
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));
});
li.appendChild(input);
li.appendChild(saveBtn);
}
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;
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);
}