Goal-Calibration/public/js/text.js

278 lines
8.8 KiB
JavaScript

const expandedChildByParent = new Map();
let activeNodeId = null;
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(function (response) {
if (!response.ok) {
return null;
}
return response.json();
})
.then(nodes => {
if (!Array.isArray(nodes)) return;
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;
titleSpan.addEventListener('click', () => {
if (activeNodeId === node.id) {
activeNodeId = null;
li.classList.remove('is-active');
closeAllAddForms();
return;
}
const previousActive = document.querySelector(
'#text-detail li.is-active'
);
if (previousActive) {
previousActive.classList.remove('is-active');
}
closeAllAddForms();
li.classList.add('is-active');
activeNodeId = node.id;
});
li.appendChild(titleSpan);
if (node.id === activeNodeId) {
li.classList.add('is-active');
}
const addBtn = document.createElement('button');
addBtn.textContent = 'Add child';
addBtn.className = 'add-child';
addBtn.addEventListener(
'click',
() => toggleAddForm(li, node.id, node.parentNodeId, 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, node.parentNodeId, textId)
);
li.appendChild(bulkBtn);
if (node.children.length > 0) {
const childUl = renderTree(node.children, textId, depth + 1);
const childrenVisible =
depth === 0 ||
expandedChildByParent.get(node.parentNodeId) === node.id;
childUl.style.display = childrenVisible ? '' : 'none';
const toggleBtn = document.createElement('button');
toggleBtn.className = 'toggle-children';
toggleBtn.textContent = childrenVisible ? '▼' : '▶';
toggleBtn.addEventListener('click', () => {
if (depth === 0) {
return;
}
const expandedSibling =
expandedChildByParent.get(node.parentNodeId);
if (expandedSibling === node.id) {
expandedChildByParent.delete(node.parentNodeId);
} else {
expandedChildByParent.set(node.parentNodeId, node.id);
}
fetchAndRenderNodes(textId);
});
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, grandparentId, 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;
expandedChildByParent.set(grandparentId, 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, grandparentId, 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;
expandedChildByParent.set(grandparentId, 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();
}