672 lines
20 KiB
PHP
672 lines
20 KiB
PHP
<?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";
|
||
?>
|