Thesaurus/Treeview.php
2026-02-23 16:11:35 +01:00

672 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
include "templates/Header.html";
?>
<!-- Hidden Fields -->
<input type="hidden" id="locale" value="de-DE">
<input type="hidden" id="ID" value="">
<input type="hidden" id="authType" value="Classification">
<!-- jsTree CSS -->
<link rel="stylesheet" href="/libs/jstree/current/themes/default/style.min.css" onerror="this.onerror=null; this.href='https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.16/themes/default/style.min.css';" />
<style>
/* Treeview Container */
.treeview-container {
display: flex;
gap: 20px;
min-height: 600px;
align-items: flex-start;
}
.tree-panel {
flex: 3;
min-width: 0;
}
.detail-panel {
flex: 2;
min-width: 0;
position: sticky;
top: 20px;
}
/* Tree Styling */
#classification-tree {
padding: 15px;
background: var(--bg-light);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
min-height: 500px;
max-height: 75vh;
overflow-x: auto;
overflow-y: auto;
white-space: nowrap;
}
/* jsTree Anpassungen für BIBB-Blau - verstärkt */
.jstree-default .jstree-clicked,
.jstree-default .jstree-wholerow-clicked {
background: #1a3a5c !important;
color: white !important;
border-radius: 4px;
box-shadow: none !important;
}
.jstree-default .jstree-clicked .jstree-icon,
.jstree-default .jstree-wholerow-clicked + .jstree-anchor .jstree-icon {
color: white !important;
}
.jstree-default .jstree-hovered,
.jstree-default .jstree-wholerow-hovered {
background: #e9ecef !important;
border-radius: 4px;
box-shadow: none !important;
}
.jstree-default .jstree-anchor {
color: var(--text-primary);
padding: 4px 8px;
white-space: nowrap;
max-width: none;
overflow: visible;
}
.jstree-default .jstree-icon {
color: var(--primary-color);
}
/* Detail Panel */
.detail-card {
background: var(--bg-light);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
padding: 20px;
min-height: 500px;
}
.detail-card h4 {
color: var(--primary-color);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary-color);
}
.detail-row {
display: flex;
margin-bottom: 15px;
}
.detail-label {
font-weight: 600;
color: var(--text-muted);
width: 120px;
flex-shrink: 0;
}
.detail-value {
color: var(--text-primary);
flex: 1;
}
.no-selection {
text-align: center;
color: var(--text-muted);
padding: 50px 20px;
}
.no-selection i {
font-size: 48px;
margin-bottom: 15px;
display: block;
opacity: 0.5;
}
/* Toolbar */
.tree-toolbar {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.tree-toolbar .btn {
display: flex;
align-items: center;
gap: 5px;
}
/* Search */
.tree-search {
position: relative;
flex: 1;
min-width: 200px;
}
.tree-search input {
width: 100%;
padding-left: 35px;
}
.tree-search i.fa-search {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
z-index: 10;
}
/* Suchergebnisse Dropdown */
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-height: 300px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.search-results.show {
display: block;
}
.search-result-item {
padding: 10px 15px;
cursor: pointer;
border-bottom: 1px solid var(--border-light);
}
.search-result-item:hover {
background: var(--bg-light);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item .result-text {
font-weight: 500;
color: var(--text-primary);
}
.search-result-item .result-id {
font-size: 12px;
color: var(--text-muted);
}
.search-no-results {
padding: 15px;
text-align: center;
color: var(--text-muted);
}
/* Responsive */
@media (max-width: 1200px) {
.treeview-container {
flex-direction: column;
}
.tree-panel, .detail-panel {
min-width: 100%;
flex: none
}
.tree-panel, .detail-panel {
min-width: 100%;
flex: none;
}
.detail-panel {
position: static; /* kein sticky bei gestapelter Ansicht */
}
}
</style>
<!-- Main Content -->
<main class="main-content">
<div class="container">
<!-- Content Card -->
<div class="card">
<div class="d-flex justify-between align-center mb-20">
<h1 class="card-title" style="margin: 0;">🌳 Klassifikationsbaum</h1>
</div>
<!-- Toolbar -->
<div class="tree-toolbar">
<div class="tree-search">
<i class="fas fa-search"></i>
<input type="text" id="tree-search-input" class="form-control" placeholder="Klassifikation suchen..." autocomplete="off" />
<div id="search-results" class="search-results"></div>
</div>
<button class="btn btn-secondary" id="btn-expand-all" title="Alle aufklappen">
<i class="fas fa-expand-alt"></i> Alle öffnen
</button>
<button class="btn btn-secondary" id="btn-collapse-all" title="Alle zuklappen">
<i class="fas fa-compress-alt"></i> Alle schließen
</button>
<button class="btn btn-secondary" id="btn-refresh" title="Aktualisieren">
<i class="fas fa-sync"></i>
</button>
</div>
<!-- Treeview Container -->
<div class="treeview-container">
<!-- Tree Panel -->
<div class="tree-panel">
<div id="classification-tree"></div>
</div>
<!-- Detail Panel -->
<div class="detail-panel">
<div class="detail-card">
<div id="detail-content">
<div class="no-selection">
<i class="fas fa-hand-pointer"></i>
<p>Wählen Sie eine Klassifikation aus dem Baum aus, um Details anzuzeigen.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- jsTree JS -->
<script src="/libs/jstree/current/jstree.min.js"></script>
<script>if (typeof $.jstree === "undefined") { document.write('<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.16/jstree.min.js"><\/script>'); }</script>
<script>
$(document).ready(function() {
// Tree initialisieren
initTree();
// Toolbar Events
$('#btn-expand-all').on('click', function() {
$('#classification-tree').jstree('open_all');
});
$('#btn-collapse-all').on('click', function() {
$('#classification-tree').jstree('close_all');
});
$('#btn-refresh').on('click', function() {
$('#classification-tree').jstree('refresh');
});
// Serverseitige Suche
var searchTimeout = null;
$('#tree-search-input').on('keyup', function() {
var searchString = $(this).val().trim();
if (searchTimeout) {
clearTimeout(searchTimeout);
}
if (searchString.length < 2) {
$('#search-results').removeClass('show').empty();
return;
}
searchTimeout = setTimeout(function() {
performSearch(searchString);
}, 300);
});
// Suche schließen bei Klick außerhalb
$(document).on('click', function(e) {
if (!$(e.target).closest('.tree-search').length) {
$('#search-results').removeClass('show');
}
});
// Focus auf Suchfeld zeigt Ergebnisse wieder
$('#tree-search-input').on('focus', function() {
if ($('#search-results').children().length > 0) {
$('#search-results').addClass('show');
}
});
});
function initTree() {
$('#classification-tree').jstree({
'core': {
'data': {
'url': function(node) {
return '/Thesaurus/ajax/getTreeData.php';
},
'data': function(node) {
return {
'id': node.id
};
}
},
'themes': {
'name': 'default',
'responsive': true,
'dots': true,
'icons': true
},
'multiple': false,
'animation': 200
},
'plugins': ['wholerow', 'sort'],
'sort': function(a, b) {
var nodeA = this.get_text(a);
var nodeB = this.get_text(b);
return nodeA.localeCompare(nodeB, 'de', {numeric: true});
}
});
// Node ausgewählt
$('#classification-tree').on('select_node.jstree', function(e, data) {
var node = data.node;
loadDetails(node.id, node.text, node.data);
});
// Tree geladen
$('#classification-tree').on('loaded.jstree', function() {
console.log('✅ Klassifikationsbaum geladen');
});
}
function performSearch(searchString) {
console.log('🔍 Suche nach:', searchString);
$.get('/Thesaurus/ajax/searchTreeData.php', {
q: searchString
}, function(data) {
var results = typeof data === 'string' ? JSON.parse(data) : data;
var html = '';
if (results && results.length > 0) {
results.forEach(function(item) {
html += '<div class="search-result-item" data-id="' + item.id + '">';
html += '<div class="result-text">' + escapeHtml(item.text) + '</div>';
html += '<div class="result-id">ID: ' + item.id + '</div>';
html += '</div>';
});
} else {
html = '<div class="search-no-results"><i class="fas fa-search"></i> Keine Ergebnisse gefunden</div>';
}
$('#search-results').html(html).addClass('show');
// Klick auf Suchergebnis
$('.search-result-item').on('click', function() {
var id = $(this).data('id');
selectAndShowNode(id);
$('#search-results').removeClass('show');
$('#tree-search-input').val('');
});
}).fail(function() {
$('#search-results').html('<div class="search-no-results">Fehler bei der Suche</div>').addClass('show');
});
}
// PATCH für Treeview.php Funktion selectAndShowNode ersetzen (v3)
// Ersetzt die bisherige selectAndShowNode() und activateNode() komplett.
function selectAndShowNode(id) {
id = String(id);
$.get('/Thesaurus/ajax/getTreePath.php', { id: id }, function(path) {
if (!path || path.length === 0) {
console.warn('⚠️ Kein Pfad gefunden für ID:', id);
loadDetails(id, '', {});
return;
}
console.log('📂 Pfad zum Knoten:', path);
// Vorfahren = alle Elemente außer dem Zielknoten selbst
var ancestors = path.slice(0, -1).map(String);
var targetId = String(path[path.length - 1]);
// Sequenziell öffnen: erst wenn ein Knoten vollständig geöffnet
// (und seine Kinder geladen) sind, wird der nächste geöffnet.
openSequential(ancestors, 0, function() {
activateNode(targetId);
});
}).fail(function() {
console.error('❌ getTreePath.php fehlgeschlagen');
loadDetails(id, '', {});
});
}
/**
* Öffnet Knoten aus dem ancestors-Array nacheinander.
* Wartet nach jedem open_node auf after_open.jstree bevor weitergemacht wird.
* Knoten die bereits offen sind werden übersprungen.
*/
function openSequential(ancestors, index, callback) {
if (index >= ancestors.length) {
callback();
return;
}
var tree = $('#classification-tree').jstree(true);
var nodeId = ancestors[index];
var node = tree.get_node(nodeId);
// Knoten bereits offen oder nicht auffindbar → nächster
if (!node || node.state.opened) {
console.log('⏭️ Überspringe (bereits offen oder nicht geladen):', nodeId);
openSequential(ancestors, index + 1, callback);
return;
}
console.log('🔓 Öffne Knoten:', nodeId);
// Einmaliger after_open-Handler nur für diesen Knoten
function onOpen(e, data) {
if (String(data.node.id) !== nodeId) return; // anderer Knoten
$('#classification-tree').off('after_open.jstree', onOpen);
console.log('✅ Geöffnet:', nodeId);
openSequential(ancestors, index + 1, callback);
}
$('#classification-tree').on('after_open.jstree', onOpen);
// Sicherheits-Timeout pro Knoten (3 Sekunden)
setTimeout(function() {
$('#classification-tree').off('after_open.jstree', onOpen);
console.warn('⏱️ Timeout beim Öffnen von:', nodeId);
openSequential(ancestors, index + 1, callback);
}, 3000);
tree.open_node(nodeId);
}
/**
* Wählt den Zielknoten aus und scrollt ihn in Sicht.
*/
function activateNode(id) {
var tree = $('#classification-tree').jstree(true);
console.log('🎯 Aktiviere Knoten:', id);
tree.deselect_all();
tree.select_node(id);
setTimeout(function() {
var anchor = document.getElementById(id + '_anchor');
if (anchor) {
anchor.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.log('📜 Gescrollt zu:', id);
} else {
console.warn('⚠️ Anchor nicht gefunden für ID:', id);
}
}, 200);
}
function loadDetails(id, text, nodeData) {
console.log('📖 Lade Details für ID:', id);
// Loading anzeigen
$('#detail-content').html('<div class="text-center p-4"><div class="spinner-border text-primary" role="status"></div><p class="mt-2">Laden...</p></div>');
$.get('/Thesaurus/ajax/getDetailAuthorityData.php', {
authType: 'Classification',
id: id,
offset: 0,
limit: 1
}, function(data, status) {
if (status === 'success') {
try {
var metadata = typeof data === 'string' ? JSON.parse(data) : data;
var row = metadata['rows'] ? metadata['rows'][0] : null;
if (row) {
renderDetails(row);
} else {
// Fallback: Zeige zumindest die Basis-Infos aus dem Baum
renderBasicDetails(id, text, nodeData);
}
} catch (e) {
console.error('❌ Parse-Fehler:', e);
renderBasicDetails(id, text, nodeData);
}
}
}).fail(function() {
renderBasicDetails(id, text, nodeData);
});
}
function renderDetails(row) {
var html = '';
html += '<h4><i class="fas fa-folder-tree me-2"></i>' + escapeHtml(row.Text || row.name || 'Unbekannt') + '</h4>';
html += '<div class="detail-row">';
html += '<span class="detail-label">ID:</span>';
html += '<span class="detail-value">' + escapeHtml(row.ID || '') + '</span>';
html += '</div>';
if (row.Notation) {
html += '<div class="detail-row">';
html += '<span class="detail-label">Notation:</span>';
html += '<span class="detail-value"><strong>' + escapeHtml(row.Notation) + '</strong></span>';
html += '</div>';
}
/* if (row.Descriptor) {
html += '<div class="detail-row">';
html += '<span class="detail-label">Deskriptor:</span>';
html += '<span class="detail-value">' + escapeHtml(row.Descriptor) + '</span>';
html += '</div>';
}
*/
if (row.Type) {
html += '<div class="detail-row">';
html += '<span class="detail-label">Typ:</span>';
html += '<span class="detail-value">' + escapeHtml(row.Type) + '</span>';
html += '</div>';
}
if (row.Scopenote) {
html += '<div class="detail-row">';
html += '<span class="detail-label">Anmerkung:</span>';
html += '<span class="detail-value">' + escapeHtml(row.Scopenote) + '</span>';
html += '</div>';
}
// Relationen
if (row.Relations && row.Relations.length > 0) {
html += '<hr>';
html += '<h5><i class="fas fa-link me-2"></i>Relationen</h5>';
html += '<div class="relations-list">';
row.Relations.forEach(function(rel) {
html += '<div class="relation-item" style="display: flex; align-items: center; padding: 8px 12px; background: white; border-radius: 4px; margin-bottom: 8px; border: 1px solid #dee2e6;">';
html += '<span style="font-weight: 600; color: var(--primary-color); width: 80px; flex-shrink: 0;">' + escapeHtml(rel.Relationtype || '') + '</span>';
html += '<span style="flex: 1;">';
html += '<a href="javascript:void(0)" onclick="selectAndShowNode(\'' + rel.IDRelation + '\')" style="color: var(--primary-color); cursor: pointer;">' + escapeHtml(rel.TextRelation || '') + '</a>';
html += ' <small class="text-muted">(ID: ' + escapeHtml(rel.IDRelation || '') + ')</small>';
html += '</span>';
html += '</div>';
});
html += '</div>';
}
// Aktionen
html += '<hr>';
html += '<div class="d-flex gap-2">';
html += '<button class="btn btn-primary btn-sm" onclick="editClassification(\'' + row.ID + '\')"><i class="fas fa-edit me-1"></i>Bearbeiten</button>';
html += '<button class="btn btn-secondary btn-sm" onclick="copyToClipboard(\'' + escapeHtml(row.Text || '') + '\')"><i class="fas fa-copy me-1"></i>Name kopieren</button>';
html += '</div>';
$('#detail-content').html(html);
}
function renderBasicDetails(id, text, nodeData) {
var html = '';
html += '<h4><i class="fas fa-folder-tree me-2"></i>' + escapeHtml(text || 'Klassifikation') + '</h4>';
html += '<div class="detail-row">';
html += '<span class="detail-label">ID:</span>';
html += '<span class="detail-value">' + escapeHtml(id) + '</span>';
html += '</div>';
if (nodeData && nodeData.name) {
html += '<div class="detail-row">';
html += '<span class="detail-label">Name:</span>';
html += '<span class="detail-value">' + escapeHtml(nodeData.name) + '</span>';
html += '</div>';
}
html += '<hr>';
html += '<div class="d-flex gap-2">';
html += '<button class="btn btn-primary btn-sm" onclick="editClassification(\'' + id + '\')"><i class="fas fa-edit me-1"></i>Bearbeiten</button>';
html += '<button class="btn btn-secondary btn-sm" onclick="copyToClipboard(\'' + escapeHtml(text || '') + '\')"><i class="fas fa-copy me-1"></i>Name kopieren</button>';
html += '</div>';
$('#detail-content').html(html);
}
function editClassification(id) {
window.location.href = 'Classifications.php?edit=' + id;
}
function copyToClipboard(text) {
var tempInput = document.createElement('input');
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
// Feedback
alert('"' + text + '" wurde in die Zwischenablage kopiert.');
}
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
<?php
include "templates/Footer.html";
?>