forensic-pathways/src/components/ToolMatrix.astro
overcuriousity 0adabad94d UI overhaul
2025-07-27 17:31:15 +02:00

804 lines
34 KiB
Plaintext

---
import { getToolsData } from '../utils/dataService.js';
import ShareButton from './ShareButton.astro';
const data = await getToolsData();
const domains = data.domains;
const phases = data.phases;
const tools = data.tools;
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
const domainAgnosticTools = domainAgnosticSoftware.map((section: any) => ({
section,
tools: tools.filter((tool: any) =>
tool['domain-agnostic-software'] && tool['domain-agnostic-software'].includes(section.id)
)
}));
const matrix: Record<string, Record<string, any[]>> = {};
domains.forEach((domain: any) => {
matrix[domain.id] = {};
phases.forEach((phase: any) => {
matrix[domain.id][phase.id] = tools.filter((tool: any) =>
tool.type !== 'concept' &&
tool.domains && tool.domains.includes(domain.id) &&
tool.phases && tool.phases.includes(phase.id)
);
});
});
---
<div id="matrix-container" class="matrix-wrapper" style="display: none;">
{domainAgnosticTools.map((sectionData: any, index: number) => (
<div id={`domain-agnostic-section-${sectionData.section.id}`} class="card collaboration-section-collapsed mb-6 border-l-4" style="border-left-color: var(--color-accent);">
<div class="collaboration-header cursor-pointer flex items-center gap-3" onclick={`toggleDomainAgnosticSection('${sectionData.section.id}')`} style="margin-bottom: 0.1rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2" class="mr-2">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
<h3 class="m-0 text-lg" style="color: var(--color-accent);">
{sectionData.section.name}
<span id={`count-${sectionData.section.id}`} class="badge text-xs" style="background-color: var(--color-text-secondary); color: var(--color-bg); margin-left: 0.5rem;">
{sectionData.tools.length}
</span>
</h3>
<div class="collaboration-expand-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</div>
<div class="collaboration-content" style="display: none;">
<div class="collaboration-tools-compact" id={`domain-agnostic-tools-${sectionData.section.id}`}>
{sectionData.tools.map((tool: any) => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
return (
<div class={`collaboration-tool-compact cursor-pointer ${hasValidProjectUrl ? 'hosted' : tool.license !== 'Proprietary' ? 'oss' : ''}`}
onclick={`window.showToolDetails('${tool.name}')`}>
<div class="tool-compact-header">
<h4 class="m-0 text-sm font-semibold">
{tool.icon && <span class="mr-2">{tool.icon}</span>}
{tool.name}
</h4>
<div class="flex gap-1">
{hasValidProjectUrl && <span class="badge badge--mini badge-primary">CC24-Server</span>}
{tool.knowledgebase === true && <span class="badge badge--mini badge-error">📖</span>}
</div>
</div>
<p class="text-muted">
{tool.description}
</p>
<div class="flex gap-3 text-xs" style="color: var(--color-text-secondary);">
<span>{tool.platforms.join(', ')}</span>
<span>•</span>
<span>{tool.skillLevel}</span>
</div>
</div>
);
})}
</div>
</div>
</div>
))}
<div id="dfir-matrix-section">
<h2 class="mb-4" style="color: var(--color-text);">MATRIX</h2>
<table class="matrix-table">
<thead>
<tr>
<th style="width: 200px;">Domain / Phase</th>
{phases.map((phase: any) => (
<th data-phase={phase.id}>{phase.name}</th>
))}
</tr>
</thead>
<tbody>
{domains.map((domain: any) => (
<tr data-domain={domain.id}>
<th>{domain.name}</th>
{phases.map((phase: any) => (
<td class="matrix-cell" data-domain={domain.id} data-phase={phase.id}>
{matrix[domain.id][phase.id].map((tool: any) => {
const isMethod = tool.type === 'method';
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const chipClass = isMethod ? 'tool-chip-method' :
hasValidProjectUrl ? 'tool-chip-hosted' :
tool.license !== 'Proprietary' ? 'tool-chip-oss' : '';
return (
<span
class={`tool-chip ${chipClass}`}
data-tool-name={tool.name}
onclick={`window.showToolDetails('${tool.name}')`}
title={`${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`}
>
{tool.name}
{tool.knowledgebase === true && <span class="text-xs" style="margin-left: 0.25rem;">📖</span>}
</span>
);
})}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
<div class="modal-overlay" id="modal-overlay" onclick="window.hideAllToolDetails()"></div>
<div class="tool-details" id="tool-details-primary">
<div class="flex justify-between items-start mb-4">
<h2 id="tool-name-primary" class="m-0">Tool Name</h2>
<div class="flex items-center gap-2">
<div id="share-button-primary" style="display: none;">
</div>
<div id="contribute-button-primary" style="display: none;">
</div>
<button class="btn-icon" onclick="window.hideToolDetails('primary')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<p id="tool-description-primary" class="text-muted"></p>
<div id="tool-badges-primary" class="flex gap-2 mb-4"></div>
<div id="tool-metadata-primary" class="mb-4"></div>
<div id="tool-tags-primary" class="mb-4"></div>
<div id="tool-links-primary" class="flex flex-col gap-2"></div>
</div>
<div class="tool-details" id="tool-details-secondary">
<div class="flex justify-between items-start mb-4">
<h2 id="tool-name-secondary" class="m-0">Tool Name</h2>
<div class="flex items-center gap-2">
<div id="share-button-secondary" style="display: none;">
</div>
<div id="contribute-button-secondary" style="display: none;">
</div>
<button class="btn-icon" onclick="window.hideToolDetails('secondary')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<p id="tool-description-secondary" class="text-muted"></p>
<div id="tool-badges-secondary" class="flex gap-2 mb-4"></div>
<div id="tool-metadata-secondary" class="mb-4"></div>
<div id="tool-tags-secondary" class="mb-4"></div>
<div id="tool-links-secondary" class="flex flex-col gap-2"></div>
</div>
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
function getSelectedPhase() {
const activePhaseButton = document.querySelector('.phase-button.active');
return activePhaseButton ? activePhaseButton.getAttribute('data-phase') : '';
}
function getSelectedDomain() {
const domainSelect = document.getElementById('domain-select');
return domainSelect ? domainSelect.value : '';
}
function updateMatrixHighlighting() {
const matrixTable = document.querySelector('.matrix-table');
if (!matrixTable) return;
matrixTable.querySelectorAll('.highlight-row, .highlight-column').forEach(el => {
el.classList.remove('highlight-row', 'highlight-column');
});
const selectedDomain = getSelectedDomain();
const selectedPhase = getSelectedPhase();
if (selectedDomain) {
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
if (domainRow) {
domainRow.classList.add('highlight-row');
}
}
if (selectedPhase) {
const phaseHeaders = matrixTable.querySelectorAll('thead th[data-phase]');
const phaseIndex = Array.from(phaseHeaders).findIndex(th =>
th.getAttribute('data-phase') === selectedPhase
);
if (phaseIndex >= 0) {
const columnIndex = phaseIndex + 1;
matrixTable.querySelectorAll(`tr`).forEach(row => {
const cell = row.children[columnIndex];
if (cell) {
cell.classList.add('highlight-column');
}
});
}
}
}
function toggleDomainAgnosticSection(sectionId) {
const section = document.getElementById(`domain-agnostic-section-${sectionId}`);
const content = section?.querySelector('.collaboration-content');
const icon = section?.querySelector('.collaboration-expand-icon svg');
if (!section || !content || !icon) return;
const isExpanded = section.classList.contains('collaboration-section-expanded');
if (isExpanded) {
section.classList.remove('collaboration-section-expanded');
section.classList.add('collaboration-section-collapsed');
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
} else {
section.classList.remove('collaboration-section-collapsed');
section.classList.add('collaboration-section-expanded');
content.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
}
}
function generateShareURL(toolName, view, modal = null) {
const toolSlug = window.createToolSlug(toolName);
const baseUrl = window.location.origin + window.location.pathname;
const params = new URLSearchParams();
params.set('tool', toolSlug);
params.set('view', view);
if (modal) {
params.set('modal', modal);
}
return `${baseUrl}?${params.toString()}`;
}
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
const originalHTML = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"/></svg> Kopiert!';
button.style.color = 'var(--color-accent)';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = '';
}, 2000);
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}
window.showShareDialog = function(shareButton) {
const toolName = shareButton.getAttribute('data-tool-name');
const context = shareButton.getAttribute('data-context');
let backdrop = document.getElementById('share-modal-backdrop');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.id = 'share-modal-backdrop';
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
`;
document.body.appendChild(backdrop);
}
const dialog = document.createElement('div');
dialog.style.cssText = `
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
max-width: 28rem;
width: 90%;
box-shadow: var(--shadow-lg);
transform: scale(0.9);
transition: transform 0.2s ease;
`;
dialog.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<h3 style="margin: 0; color: var(--color-primary); display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
${toolName} teilen
</h3>
<button id="close-share-dialog" class="btn-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<button class="share-option-btn" data-url="${generateShareURL(toolName, 'grid')}" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background-color: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;" onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'" onmouseout="this.style.backgroundColor='var(--color-bg)'">
<div style="width: 2rem; height: 2rem; background-color: var(--color-primary); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
</div>
<div>
<div style="font-weight: 500; margin-bottom: 0.25rem; color: var(--color-text);">Kachelansicht</div>
<div style="font-size: 0.875rem; color: var(--color-text-secondary);">Scrollt zur Karte in der Übersicht</div>
</div>
</button>
<button class="share-option-btn" data-url="${generateShareURL(toolName, 'matrix')}" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background-color: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;" onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'" onmouseout="this.style.backgroundColor='var(--color-bg)'">
<div style="width: 2rem; height: 2rem; background-color: var(--color-accent); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<rect x="3" y="3" width="4" height="4"/>
<rect x="10" y="3" width="4" height="4"/>
<rect x="17" y="3" width="4" height="4"/>
<rect x="3" y="10" width="4" height="4"/>
<rect x="10" y="10" width="4" height="4"/>
<rect x="17" y="10" width="4" height="4"/>
<rect x="3" y="17" width="4" height="4"/>
<rect x="10" y="17" width="4" height="4"/>
<rect x="17" y="17" width="4" height="4"/>
</svg>
</div>
<div>
<div style="font-weight: 500; margin-bottom: 0.25rem; color: var(--color-text);">Matrix-Ansicht</div>
<div style="font-size: 0.875rem; color: var(--color-text-secondary);">Zeigt Tool-Position in der Matrix</div>
</div>
</button>
<button class="share-option-btn" data-url="${generateShareURL(toolName, 'modal', 'primary')}" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background-color: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;" onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'" onmouseout="this.style.backgroundColor='var(--color-bg)'">
<div style="width: 2rem; height: 2rem; background-color: var(--color-warning); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
</div>
<div>
<div style="font-weight: 500; margin-bottom: 0.25rem; color: var(--color-text);">Tool-Details</div>
<div style="font-size: 0.875rem; color: var(--color-text-secondary);">Öffnet Detail-Fenster direkt</div>
</div>
</button>
</div>
`;
backdrop.appendChild(dialog);
requestAnimationFrame(() => {
backdrop.style.opacity = '1';
dialog.style.transform = 'scale(1)';
});
const closeDialog = () => {
backdrop.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
if (backdrop.parentNode) {
document.body.removeChild(backdrop);
}
}, 200);
};
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) closeDialog();
});
document.getElementById('close-share-dialog').addEventListener('click', closeDialog);
dialog.querySelectorAll('.share-option-btn').forEach(btn => {
btn.addEventListener('click', () => {
const url = btn.getAttribute('data-url');
copyToClipboard(url, btn);
});
});
};
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
window.showToolDetails = function(toolName, modalType = 'primary') {
const tool = toolsData.find(t => t.name === toolName);
if (!tool) {
console.error('Tool not found:', toolName);
return;
}
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
const elements = {
name: document.getElementById(`tool-name-${modalType}`),
description: document.getElementById(`tool-description-${modalType}`),
badges: document.getElementById(`tool-badges-${modalType}`),
metadata: document.getElementById(`tool-metadata-${modalType}`),
tags: document.getElementById(`tool-tags-${modalType}`),
links: document.getElementById(`tool-links-${modalType}`)
};
for (const [key, element] of Object.entries(elements)) {
if (!element) {
console.error(`Element not found: tool-${key}-${modalType}`);
return;
}
}
const iconHtml = tool.icon ? `<span class="mr-3 text-xl">${tool.icon}</span>` : '';
elements.name.innerHTML = `${iconHtml}${tool.name}`;
elements.description.textContent = tool.description;
const hasValidProjectUrl = window.isToolHosted(tool);
elements.badges.innerHTML = '';
if (isConcept) {
elements.badges.innerHTML += '<span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>';
} else if (isMethod) {
elements.badges.innerHTML += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
} else if (hasValidProjectUrl) {
elements.badges.innerHTML += '<span class="badge badge-primary">CC24-Server</span>';
}
if (tool.knowledgebase === true) {
elements.badges.innerHTML += '<span class="badge badge-error">📖</span>';
}
const domains = tool.domains || [];
const phases = tool.phases || [];
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
const phasesText = phases.join(', ');
let metadataHTML = `<div class="grid gap-2">`;
if (!isConcept) {
metadataHTML += `
<div><strong>Betriebssystem:</strong> ${(tool.platforms || []).join(', ')}</div>
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
<div><strong>Deployment:</strong> ${tool.accessType}</div>
`;
} else {
metadataHTML += `<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>`;
}
metadataHTML += `
<div><strong>Einsatzgebiete:</strong> ${domainsText}</div>
<div><strong>Ermittlungsphasen:</strong> ${phasesText}</div>
</div>`;
elements.metadata.innerHTML = metadataHTML;
const tags = tool.tags || [];
let tagsHTML = `
<div class="flex flex-wrap gap-1">
${tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
`;
const relatedConcepts = tool.related_concepts || [];
if (relatedConcepts.length > 0 && modalType === 'primary') {
const conceptLinks = relatedConcepts.map(conceptName => {
const concept = toolsData.find(t => t.name === conceptName && t.type === 'concept');
if (concept) {
return `<button class="tag cursor-pointer" style="background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); color: var(--color-concept); transition: var(--transition-fast);"
onclick="event.stopPropagation(); window.showToolDetails('${conceptName}', 'secondary')"
onmouseover="this.style.backgroundColor='var(--color-concept)'; this.style.color='white';"
onmouseout="this.style.backgroundColor='var(--color-concept-bg)'; this.style.color='var(--color-concept)';">
${conceptName}
</button>`;
}
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary);">${conceptName}</span>`;
}).join('');
const isMobile = window.innerWidth <= 768;
const collapseOnMobile = isMobile && relatedConcepts.length > 2;
tagsHTML += `
<div class="mt-4">
<div class="flex items-center gap-2 mb-2">
<strong style="color: var(--color-text);">Verwandte Konzepte:</strong>
${collapseOnMobile ? `
<button id="concepts-toggle-${modalType}"
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
class="btn-icon text-xs">
</button>
` : ''}
</div>
<div ${collapseOnMobile ? 'style="display: none;"' : ''} class="flex flex-wrap gap-1">
${conceptLinks}
</div>
</div>
`;
}
elements.tags.innerHTML = tagsHTML;
let linksHTML = '';
if (isConcept) {
linksHTML += `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary w-full" style="background-color: var(--color-concept); border-color: var(--color-concept);">
Mehr erfahren
</a>
`;
} else if (isMethod) {
linksHTML += `
<a href="${tool.projectUrl || tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary w-full" style="background-color: var(--color-method); border-color: var(--color-method);">
Zur Methode
</a>
`;
} else if (hasValidProjectUrl) {
linksHTML += `
<div class="flex gap-2">
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary flex-1">
Homepage
</a>
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary flex-1">
Zugreifen
</a>
</div>
`;
} else {
linksHTML += `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary w-full">
Software-Homepage
</a>
`;
}
if (tool.knowledgebase === true) {
const kbId = window.createToolSlug(tool.name);
linksHTML += `
<a href="/knowledgebase#kb-${kbId}" class="btn btn-secondary w-full mt-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Knowledgebase anzeigen
</a>
`;
}
elements.links.innerHTML = linksHTML;
const shareButtonContainer = document.getElementById(`share-button-${modalType}`);
if (shareButtonContainer) {
const toolSlug = window.createToolSlug(tool.name);
shareButtonContainer.innerHTML = `
<button class="share-btn share-btn--medium"
data-tool-name="${tool.name}"
data-tool-slug="${toolSlug}"
data-context="modal-${modalType}"
onclick="event.stopPropagation(); window.showShareDialog(this)"
title="${tool.name} teilen"
aria-label="${tool.name} teilen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</button>
`;
shareButtonContainer.style.display = 'block';
}
const contributeButtonContainer = document.getElementById(`contribute-button-${modalType}`);
if (contributeButtonContainer) {
contributeButtonContainer.innerHTML = `
<a href="/contribute/tool?edit=${encodeURIComponent(tool.name)}"
class="btn-icon"
data-contribute-button="edit"
data-tool-name="${tool.name}"
title="Edit ${tool.name}"
aria-label="Edit ${tool.name}"
onclick="event.stopPropagation();">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</a>
`;
contributeButtonContainer.style.display = 'block';
}
const overlay = document.getElementById('modal-overlay');
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
if (overlay) overlay.classList.add('active');
if (modalType === 'primary' && primaryModal) primaryModal.classList.add('active');
if (modalType === 'secondary' && secondaryModal) secondaryModal.classList.add('active');
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
if (primaryActive && secondaryActive) {
document.body.classList.add('modals-side-by-side');
}
};
window.hideToolDetails = function(modalType = 'both') {
const overlay = document.getElementById('modal-overlay');
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
// Debounce rapid calls
if (window.modalHideInProgress) return;
window.modalHideInProgress = true;
setTimeout(() => {
window.modalHideInProgress = false;
}, 100);
if (modalType === 'both' || modalType === 'all') {
if (primaryModal) {
primaryModal.classList.remove('active');
const shareButtonPrimary = document.getElementById('share-button-primary');
const contributeButtonPrimary = document.getElementById('contribute-button-primary');
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
if (contributeButtonPrimary) contributeButtonPrimary.style.display = 'none';
}
if (secondaryModal) {
secondaryModal.classList.remove('active');
const shareButtonSecondary = document.getElementById('share-button-secondary');
const contributeButtonSecondary = document.getElementById('contribute-button-secondary');
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
} else if (modalType === 'primary' && primaryModal) {
primaryModal.classList.remove('active');
const shareButtonPrimary = document.getElementById('share-button-primary');
const contributeButtonPrimary = document.getElementById('contribute-button-primary');
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
if (contributeButtonPrimary) contributeButtonPrimary.style.display = 'none';
} else if (modalType === 'secondary' && secondaryModal) {
secondaryModal.classList.remove('active');
const shareButtonSecondary = document.getElementById('share-button-secondary');
const contributeButtonSecondary = document.getElementById('contribute-button-secondary');
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
// Consolidated state checking with safety checks
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
// Update overlay and body classes atomically
if (!primaryActive && !secondaryActive) {
if (overlay) overlay.classList.remove('active');
document.body.classList.remove('modals-side-by-side');
} else if (primaryActive && secondaryActive) {
// Both active - ensure side-by-side class
document.body.classList.add('modals-side-by-side');
} else {
// Only one active - remove side-by-side class
document.body.classList.remove('modals-side-by-side');
}
};
window.hideAllToolDetails = function() {
window.hideToolDetails('both');
};
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
if (view === 'matrix') {
setTimeout(updateMatrixHighlighting, 100);
}
});
window.addEventListener('toolsFiltered', (event) => {
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
setTimeout(updateMatrixHighlighting, 50);
}
});
window.addEventListener('toolsFiltered', (event) => {
const filtered = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
const selectedPhase = getSelectedPhase();
const domainAgnosticPhaseIds = domainAgnosticSoftware.map(section => section.id);
const isDomainAgnosticPhase = domainAgnosticPhaseIds.includes(selectedPhase);
domainAgnosticSoftware.forEach(sectionData => {
const section = document.getElementById(`domain-agnostic-section-${sectionData.id}`);
const container = document.getElementById(`domain-agnostic-tools-${sectionData.id}`);
if (!section || !container) return;
});
if (!isDomainAgnosticPhase) {
document.getElementById('dfir-matrix-section').style.display = 'block';
document.querySelectorAll('.matrix-cell').forEach(cell => {
cell.innerHTML = '';
});
filtered.forEach(tool => {
if (tool.type === 'concept') {
return;
}
const isMethod = tool.type === 'method';
const hasValidProjectUrl = window.isToolHosted(tool);
const domains = tool.domains || [];
const phases = tool.phases || [];
domains.forEach(domain => {
phases.forEach(phase => {
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
if (cell) {
const chip = document.createElement('span');
const chipClass = isMethod ? 'tool-chip-method' :
hasValidProjectUrl ? 'tool-chip-hosted' :
tool.license !== 'Proprietary' ? 'tool-chip-oss' : '';
chip.className = `tool-chip ${chipClass}`;
chip.setAttribute('title', `${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`);
chip.innerHTML = `${tool.name}${tool.knowledgebase === true ? '<span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>' : ''}`;
chip.onclick = () => window.showToolDetails(tool.name);
cell.appendChild(chip);
}
});
});
});
setTimeout(updateMatrixHighlighting, 50);
}
}
});
</script>