Files
datascape/assets/tree-sidebar.js
T

201 lines
6.9 KiB
JavaScript

(function () {
// This script owns the .tree-scroll child of the aside, not the aside itself.
var container = document.querySelector('aside.tree-sidebar .tree-scroll');
if (!container) return;
function joinPath(parent, name) {
if (parent === '/' || parent === '') return '/' + name;
return parent.replace(/\/+$/, '') + '/' + name;
}
function encodeSegments(p) {
return p.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
}
function folderHref(p) {
if (p === '/' || p === '') return '/';
return '/' + encodeSegments(p) + '/';
}
function fileHref(p) {
return '/' + encodeSegments(p);
}
function fetchFolder(path, expandTo) {
var url = folderHref(path) + '?tree=1';
if (expandTo) url += '&expandTo=' + encodeURIComponent(expandTo);
return fetch(url, { credentials: 'same-origin' }).then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
});
}
// Current page as a canonical, DECODED wiki path ("/" for root, no trailing
// slash otherwise). pathname is percent-encoded; the tree's entry names (and
// thus fullPath) are decoded, so we must decode here too or paths containing
// spaces / non-ASCII never match a row and the chain never expands.
var activePath = (function () {
var p = window.location.pathname.replace(/\/+$/, '');
if (p === '') return '/';
try { return decodeURIComponent(p); } catch (e) { return p; }
})();
var activeRow = null;
function disabledRow(text) {
var row = document.createElement('div');
row.className = 'tree-row is-disabled';
row.textContent = text;
return row;
}
// renderInto fills containerEl with rows for `entries` (children of
// parentPath). Folders carrying a `children` array (the pre-expanded
// ancestor chain from ?expandTo) are opened immediately and recurse.
function renderInto(containerEl, parentPath, entries) {
if (!entries || entries.length === 0) {
containerEl.appendChild(disabledRow('(empty)'));
return;
}
entries.forEach(function (entry) {
var built = buildRow(parentPath, entry);
containerEl.appendChild(built.row);
if (built.preExpand) built.open();
});
}
function buildRow(parentPath, entry) {
var fullPath = joinPath(parentPath, entry.name);
var isFolder = entry.kind === 'folder';
var row = document.createElement('div');
row.className = 'tree-row';
var chevron = document.createElement('span');
chevron.className = 'tree-chevron';
if (isFolder) chevron.textContent = '▸'; // ▸
else chevron.classList.add('is-leaf');
row.appendChild(chevron);
var link = document.createElement('a');
link.className = 'tree-name ' + (isFolder ? 'tree-folder' : 'tree-file');
link.textContent = entry.name;
link.href = isFolder ? folderHref(fullPath) : fileHref(fullPath);
row.appendChild(link);
if (isFolder && fullPath === activePath) {
row.classList.add('is-active');
activeRow = row;
}
var preChildren = (isFolder && entry.children) ? entry.children : null;
var childrenEl = null;
var loaded = false;
var isOpen = false;
function ensureChildrenEl() {
if (!childrenEl) {
childrenEl = document.createElement('div');
childrenEl.className = 'tree-children';
}
return childrenEl;
}
function loadChildren() {
var el = ensureChildrenEl();
el.textContent = '';
el.appendChild(disabledRow('…'));
fetchFolder(fullPath).then(function (resp) {
el.textContent = '';
renderInto(el, fullPath, resp.entries);
loaded = true;
}).catch(function () {
el.textContent = '';
var err = document.createElement('div');
err.className = 'tree-row';
err.textContent = '(failed — tap to retry)';
err.addEventListener('click', function (e) {
e.stopPropagation();
loadChildren();
});
el.appendChild(err);
});
}
function open() {
if (!isFolder || isOpen) return;
var el = ensureChildrenEl();
row.parentNode.insertBefore(el, row.nextSibling);
chevron.textContent = '▾'; // ▾
isOpen = true;
if (!loaded) {
if (preChildren) {
renderInto(el, fullPath, preChildren);
loaded = true;
} else {
loadChildren();
}
}
}
function close() {
if (!isFolder || !isOpen) return;
if (childrenEl && childrenEl.parentNode) {
childrenEl.parentNode.removeChild(childrenEl);
}
chevron.textContent = '▸';
isOpen = false;
}
// Clicking the name link navigates (folder) or opens the file; clicking
// anywhere else on the row toggles expansion for folders.
row.addEventListener('click', function (e) {
if (e.target.closest('a.tree-name')) return;
if (!isFolder) return;
if (isOpen) close(); else open();
});
return { row: row, open: open, preExpand: !!preChildren };
}
function render() {
container.textContent = '';
var listing = document.createElement('div');
container.appendChild(listing);
listing.appendChild(disabledRow('…'));
fetchFolder('/', activePath === '/' ? '' : activePath).then(function (resp) {
listing.textContent = '';
renderInto(listing, '/', resp.entries);
if (activeRow) {
try { activeRow.scrollIntoView({ block: 'nearest' }); } catch (e) {}
}
}).catch(function () {
listing.textContent = '';
var err = document.createElement('div');
err.className = 'tree-row';
err.textContent = '(failed — tap to retry)';
err.addEventListener('click', function () { render(); });
listing.appendChild(err);
});
}
function setupMenuItem() {
var specials = document.querySelector('.actions-special');
if (!specials) return;
var item = document.createElement("button");
item.type = "button";
item.className = "btn btn-block";
item.title = "Folders";
item.textContent = "FOLDERS";
item.addEventListener("click", function () {
if (typeof openOverlay === "function") openOverlay(container);
});
specials.appendChild(item);
}
render();
setupMenuItem();
})();