245 lines
7.7 KiB
JavaScript
245 lines
7.7 KiB
JavaScript
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();
|
|
}
|