Files
Netbox-Documentation/netbox_documentation/templates/netbox_documentation/document_tree.html
T
MrBlakeandClaude Opus 5.5 78d60bd0ef feat: add Windows Explorer-style view for documentation
Show documentation in an Explorer-like layout: a folder navigation
pane on the left, folder and document tiles with large icons on the
right, plus back/up buttons, a clickable address bar and a status bar.
A view switcher (Explorer | Tree | List) lets users go back to the
previous tree view; the chosen view is remembered in localStorage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:14:03 +02:00

255 lines
13 KiB
HTML

{% extends 'base/layout.html' %}
{% block title %}Dokumentationen{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col d-flex justify-content-between align-items-center flex-wrap gap-2">
<input type="search" id="doc-tree-search" class="form-control" style="max-width: 20rem;" placeholder="Dokumente & Ordner durchsuchen…">
<div class="d-flex gap-2">
{% if perms.netbox_documentation.add_document %}
<a href="{% url 'plugins:netbox_documentation:document_add' %}" class="btn btn-success"><i class="mdi mdi-plus-thick"></i> Hinzufügen</a>
{% endif %}
<div class="btn-group" role="group" aria-label="Ansicht">
<button type="button" class="btn btn-outline-secondary" data-view="explorer" title="Explorer-Ansicht"><i class="mdi mdi-view-grid-outline"></i> Explorer</button>
<button type="button" class="btn btn-outline-secondary" data-view="tree" title="Baumansicht"><i class="mdi mdi-file-tree"></i> Baum</button>
<a href="{% url 'plugins:netbox_documentation:document_list' %}" class="btn btn-outline-secondary" title="Listenansicht"><i class="mdi mdi-format-list-bulleted"></i> Liste</a>
</div>
</div>
</div>
</div>
{% if tree or uncategorized %}
<div class="card netbox-explorer" id="doc-explorer" hidden>
<div class="card-header d-flex align-items-center gap-2 py-2">
<button type="button" class="btn btn-sm btn-ghost-secondary" id="doc-explorer-back" title="Zurück"><i class="mdi mdi-arrow-left"></i></button>
<button type="button" class="btn btn-sm btn-ghost-secondary" id="doc-explorer-up" title="Übergeordneter Ordner (Rücktaste)"><i class="mdi mdi-arrow-up"></i></button>
<nav class="netbox-explorer-address flex-grow-1" id="doc-explorer-address"></nav>
<a href="#" class="btn btn-sm btn-ghost-secondary d-none" id="doc-explorer-props" title="Ordnerdetails"><i class="mdi mdi-information-outline"></i></a>
</div>
<div class="netbox-explorer-body">
<aside class="netbox-explorer-nav">
<div class="netbox-explorer-nav-row" data-folder="root"><span class="netbox-explorer-twisty"></span><i class="mdi mdi-folder-home text-warning"></i><span>Dokumentationen</span></div>
<ul class="list-unstyled mb-0">
{% for node in tree %}
{% include 'netbox_documentation/inc/explorer_nav_node.html' with node=node %}
{% endfor %}
</ul>
</aside>
<section class="netbox-explorer-content">
<div class="netbox-explorer-pane" data-folder="root" data-parent="" data-title="Dokumentationen">
{% for node in tree %}
{% include 'netbox_documentation/inc/explorer_folder_tile.html' with node=node %}
{% endfor %}
{% for document in uncategorized %}
{% include 'netbox_documentation/inc/explorer_file_tile.html' with document=document %}
{% endfor %}
</div>
{% for node in tree %}
{% include 'netbox_documentation/inc/explorer_pane.html' with node=node parent='root' %}
{% endfor %}
<p class="text-muted d-none mb-0" id="doc-explorer-nomatch">Keine Treffer in diesem Ordner.</p>
</section>
</div>
<div class="card-footer py-1 small text-muted" id="doc-explorer-status"></div>
</div>
{% endif %}
<div class="card" id="doc-tree-view">
<div class="card-body">
{% if tree or uncategorized %}
<ul class="netbox-doc-tree list-unstyled mb-0">
{% for node in tree %}
{% include 'netbox_documentation/inc/document_tree_node.html' with node=node %}
{% endfor %}
{% for document in uncategorized %}
{% include 'netbox_documentation/inc/document_tree_file.html' with document=document %}
{% endfor %}
</ul>
{% else %}
<p class="text-muted mb-0">Noch keine Dokumentationen vorhanden.</p>
{% endif %}
</div>
</div>
<style>
.netbox-doc-tree { margin: 0; }
.netbox-doc-tree .netbox-doc-tree { margin-left: 1.5rem; margin-top: .35rem; border-left: 1px dashed var(--tblr-border-color, #dee2e6); padding-left: 1rem; }
.netbox-doc-tree-item { margin-bottom: .35rem; }
.netbox-doc-tree-item > details > summary { list-style: none; cursor: pointer; }
.netbox-doc-tree-item > details > summary::-webkit-details-marker { display: none; }
.netbox-doc-tree-item > details > summary::before { content: "\f0142"; font-family: "Material Design Icons"; margin-right: .35rem; display: inline-block; transition: transform .15s ease; }
.netbox-doc-tree-item > details[open] > summary::before { transform: rotate(90deg); }
.netbox-doc-tree-summary { display: flex; align-items: center; gap: .5rem; padding: .25rem .25rem; border-radius: .25rem; }
.netbox-doc-tree-summary:hover { background-color: var(--tblr-bg-surface-secondary, rgba(0,0,0,.03)); }
.netbox-doc-tree-leaf { margin-left: 1.35rem; }
.netbox-doc-tree-empty { margin-left: 1.35rem; font-size: .875rem; }
/* Explorer view */
.netbox-explorer-body { display: flex; min-height: 28rem; }
.netbox-explorer-nav { width: 16rem; flex-shrink: 0; border-right: 1px solid var(--tblr-border-color, #dee2e6); padding: .5rem 0; overflow: auto; max-height: 70vh; }
.netbox-explorer-nav ul ul { padding-left: 1rem; }
.netbox-explorer-nav-row { display: flex; align-items: center; gap: .35rem; padding: .2rem .5rem; cursor: pointer; white-space: nowrap; user-select: none; }
.netbox-explorer-nav-row:hover { background-color: var(--tblr-bg-surface-secondary, rgba(0,0,0,.04)); }
.netbox-explorer-nav-row.active { background-color: rgba(var(--tblr-primary-rgb, 32,107,196), .15); }
.netbox-explorer-twisty { width: 1rem; flex-shrink: 0; display: inline-block; text-align: center; transition: transform .15s ease; }
.netbox-explorer-twisty.open { transform: rotate(90deg); }
.netbox-explorer-content { flex-grow: 1; padding: 1rem; overflow: auto; max-height: 70vh; }
.netbox-explorer-pane { display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: .5rem; align-content: start; }
.netbox-explorer-tile { display: flex; flex-direction: column; align-items: center; text-align: center; gap: .15rem; padding: .5rem .25rem; border: 1px solid transparent; border-radius: .25rem; cursor: pointer; color: inherit; text-decoration: none; user-select: none; }
.netbox-explorer-tile:hover { background-color: rgba(var(--tblr-primary-rgb, 32,107,196), .08); border-color: rgba(var(--tblr-primary-rgb, 32,107,196), .25); text-decoration: none; color: inherit; }
.netbox-explorer-tile:focus { background-color: rgba(var(--tblr-primary-rgb, 32,107,196), .18); border-color: rgba(var(--tblr-primary-rgb, 32,107,196), .4); outline: none; }
.netbox-explorer-icon { font-size: 3.25rem; line-height: 1; }
.netbox-explorer-label { word-break: break-word; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.netbox-explorer-empty { grid-column: 1 / -1; }
.netbox-explorer-address { display: flex; align-items: center; flex-wrap: wrap; gap: .15rem; border: 1px solid var(--tblr-border-color, #dee2e6); border-radius: .25rem; padding: .15rem .5rem; min-height: 2rem; }
.netbox-explorer-address a { cursor: pointer; }
@media (max-width: 767px) { .netbox-explorer-nav { display: none; } }
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
var input = document.getElementById('doc-tree-search');
var explorer = document.getElementById('doc-explorer');
var treeView = document.getElementById('doc-tree-view');
var VIEW_KEY = 'netbox_documentation.view';
var view = 'explorer';
try { view = localStorage.getItem(VIEW_KEY) || 'explorer'; } catch (e) {}
// --- Explorer view ---
var current = 'root', backStack = [];
function pane(id) { return explorer.querySelector('.netbox-explorer-pane[data-folder="' + id + '"]'); }
function navRow(id) { return explorer.querySelector('.netbox-explorer-nav-row[data-folder="' + id + '"]'); }
function toggleNav(row, open) {
var sub = row && row.parentElement.querySelector(':scope > ul');
if (!sub) return;
var show = open === undefined ? sub.classList.contains('d-none') : open;
sub.classList.toggle('d-none', !show);
row.querySelector('.netbox-explorer-twisty').classList.toggle('open', show);
}
function openFolder(id, remember) {
var target = pane(id);
if (!target) return;
if (remember !== false && id !== current) backStack.push(current);
current = id;
explorer.querySelectorAll('.netbox-explorer-pane').forEach(function (p) {
p.classList.toggle('d-none', p !== target);
});
// Address bar (breadcrumb)
var parts = [], p = target;
while (p) { parts.unshift(p); p = p.dataset.parent ? pane(p.dataset.parent) : null; }
var address = document.getElementById('doc-explorer-address');
address.innerHTML = '';
parts.forEach(function (part, i) {
if (i) address.insertAdjacentHTML('beforeend', '<i class="mdi mdi-chevron-right text-muted"></i>');
var a = document.createElement('a');
a.textContent = part.dataset.title;
a.dataset.open = part.dataset.folder;
if (i === 0) a.insertAdjacentHTML('afterbegin', '<i class="mdi mdi-folder-home text-warning"></i> ');
address.appendChild(a);
});
// Navigation pane: highlight current folder and expand its ancestors
explorer.querySelectorAll('.netbox-explorer-nav-row.active').forEach(function (r) { r.classList.remove('active'); });
var row = navRow(id);
if (row) {
row.classList.add('active');
var li = row.parentElement.parentElement.closest('li');
while (li) {
toggleNav(li.querySelector(':scope > .netbox-explorer-nav-row'), true);
li = li.parentElement.closest('li');
}
}
document.getElementById('doc-explorer-up').disabled = !target.dataset.parent;
document.getElementById('doc-explorer-back').disabled = !backStack.length;
var props = document.getElementById('doc-explorer-props');
props.classList.toggle('d-none', !target.dataset.url);
if (target.dataset.url) props.href = target.dataset.url;
applySearch();
}
function goUp() {
var parent = pane(current).dataset.parent;
if (parent) openFolder(parent);
}
function updateStatus() {
var tiles = pane(current).querySelectorAll('.netbox-explorer-tile:not(.d-none)');
var folders = 0, files = 0;
tiles.forEach(function (t) { if (t.dataset.open) { folders++; } else { files++; } });
document.getElementById('doc-explorer-status').textContent = folders + ' Ordner, ' + files + ' Dokument(e)';
document.getElementById('doc-explorer-nomatch').classList.toggle('d-none', tiles.length > 0 || !input.value.trim());
}
function applySearch() {
if (!explorer || view !== 'explorer') return;
var term = input ? input.value.trim().toLowerCase() : '';
pane(current).querySelectorAll('.netbox-explorer-tile').forEach(function (tile) {
tile.classList.toggle('d-none', !!term && tile.dataset.name.indexOf(term) === -1);
});
updateStatus();
}
if (explorer) {
explorer.addEventListener('click', function (e) {
var row = e.target.closest('.netbox-explorer-nav-row');
if (row && e.target.closest('.netbox-explorer-twisty')) { toggleNav(row); return; }
if (row) { openFolder(row.dataset.folder); return; }
var target = e.target.closest('.netbox-explorer-address [data-open], .netbox-explorer-tile[data-open]');
if (target) openFolder(target.dataset.open);
});
explorer.addEventListener('keydown', function (e) {
var tile = e.target.closest('.netbox-explorer-tile[data-open]');
if (tile && e.key === 'Enter') openFolder(tile.dataset.open);
if (e.key === 'Backspace') { e.preventDefault(); goUp(); }
});
document.getElementById('doc-explorer-up').addEventListener('click', goUp);
document.getElementById('doc-explorer-back').addEventListener('click', function () {
if (backStack.length) openFolder(backStack.pop(), false);
});
openFolder('root', false);
}
// --- View switcher ---
function setView(v) {
view = explorer ? v : 'tree';
if (explorer) explorer.hidden = view !== 'explorer';
treeView.hidden = view === 'explorer';
document.querySelectorAll('[data-view]').forEach(function (b) {
b.classList.toggle('active', b.dataset.view === view);
});
try { localStorage.setItem(VIEW_KEY, v); } catch (e) {}
applySearch();
}
document.querySelectorAll('[data-view]').forEach(function (b) {
b.addEventListener('click', function () { setView(b.dataset.view); });
});
setView(view);
// --- Tree view search ---
if (!input) return;
input.addEventListener('input', function () {
applySearch();
var term = input.value.trim().toLowerCase();
document.querySelectorAll('.netbox-doc-tree-item').forEach(function (li) {
var match = !term || li.dataset.name.indexOf(term) !== -1;
li.classList.toggle('d-none', !match);
if (match) {
var parent = li.parentElement.closest('.netbox-doc-tree-item');
while (parent) {
parent.classList.remove('d-none');
var details = parent.querySelector(':scope > details');
if (details) details.open = true;
parent = parent.parentElement.closest('.netbox-doc-tree-item');
}
}
});
});
});
</script>
{% endblock %}