knip-html-reporter
Version:
A beautiful HTML reporter for Knip that transforms analysis results into interactive reports
308 lines (266 loc) • 8.71 kB
JavaScript
/**
* Client-side JavaScript for interactive features
* This code is embedded in the generated HTML report
*/
export function getInteractiveScript() {
return /* html */ `
<script>
(function() {
'use strict';
// State management
let state = {
searchQuery: '',
activeFilters: new Set(),
allIssueTypes: new Set(),
currentTheme: 'system'
};
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', function() {
initializeTheme();
initializeFilters();
initializeSearch();
collectIssueTypes();
});
/**
* Get the effective theme (resolving 'system' to 'light' or 'dark')
*/
function getEffectiveTheme(theme) {
if (theme === 'system') {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light';
}
return theme;
}
/**
* Initialize theme management
*/
function initializeTheme() {
// Get saved theme or default to system
const savedTheme = localStorage.getItem('knip-report-theme') || 'system';
state.currentTheme = savedTheme;
// Apply theme
applyTheme(savedTheme);
// Update button states
updateThemeButtons(savedTheme);
// Add click handlers to theme buttons
const themeButtons = document.querySelectorAll('.theme-toggle-btn');
themeButtons.forEach(btn => {
btn.addEventListener('click', function() {
const theme = this.dataset.theme;
state.currentTheme = theme;
localStorage.setItem('knip-report-theme', theme);
applyTheme(theme);
updateThemeButtons(theme);
});
});
// Listen for system theme changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (state.currentTheme === 'system') {
applyTheme('system');
}
});
}
}
/**
* Apply theme to document
*/
function applyTheme(theme) {
const root = document.documentElement;
const effectiveTheme = getEffectiveTheme(theme);
root.setAttribute('data-theme', effectiveTheme);
}
/**
* Update theme button states
*/
function updateThemeButtons(activeTheme) {
const themeButtons = document.querySelectorAll('.theme-toggle-btn');
themeButtons.forEach(btn => {
if (btn.dataset.theme === activeTheme) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
/**
* Collect all unique issue types from the page
*/
function collectIssueTypes() {
const categorySections = document.querySelectorAll('.category-section');
categorySections.forEach(el => {
const category = el.dataset.category;
if (category) state.allIssueTypes.add(category);
});
}
/**
* Initialize filter functionality via summary cards
*/
function initializeFilters() {
const summaryCards = document.querySelectorAll('.summary-card');
summaryCards.forEach(card => {
// Add click handler
card.addEventListener('click', function() {
toggleCardFilter(this);
});
// Add keyboard support
card.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleCardFilter(this);
}
});
});
}
/**
* Toggle filter when a summary card is clicked
*/
function toggleCardFilter(card) {
const filterType = card.dataset.filter;
if (!filterType) return;
// Toggle this filter
if (state.activeFilters.has(filterType)) {
state.activeFilters.delete(filterType);
card.classList.remove('active');
} else {
state.activeFilters.add(filterType);
card.classList.add('active');
}
applyFiltersAndSearch();
}
/**
* Initialize search functionality
*/
function initializeSearch() {
const searchInput = document.getElementById('search-input');
const clearBtn = document.getElementById('clear-search');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
state.searchQuery = e.target.value.toLowerCase();
if (clearBtn) {
clearBtn.style.display = state.searchQuery ? 'block' : 'none';
}
applyFiltersAndSearch();
});
}
if (clearBtn) {
clearBtn.addEventListener('click', function() {
if (searchInput) {
searchInput.value = '';
state.searchQuery = '';
this.style.display = 'none';
applyFiltersAndSearch();
}
});
}
}
/**
* Apply both filters and search
*/
function applyFiltersAndSearch() {
const categorySections = document.querySelectorAll('.category-section');
let visibleSections = 0;
categorySections.forEach(section => {
const category = section.dataset.category;
// Check if this category should be visible based on filters
const passesFilter = state.activeFilters.size === 0 ||
state.activeFilters.has(category);
if (!passesFilter) {
section.style.display = 'none';
return;
}
// If no search query, show the entire category
if (!state.searchQuery) {
section.style.display = 'block';
visibleSections++;
// Reset all table rows to visible
const rows = section.querySelectorAll('tbody tr');
rows.forEach(row => row.style.display = '');
return;
}
// Search at the individual issue (table row) level
const rows = section.querySelectorAll('tbody tr');
let visibleRowsCount = 0;
rows.forEach(row => {
if (rowMatchesSearch(row)) {
row.style.display = '';
visibleRowsCount++;
} else {
row.style.display = 'none';
}
});
// Show category if it has visible rows
if (visibleRowsCount > 0) {
section.style.display = 'block';
visibleSections++;
} else {
section.style.display = 'none';
}
});
// Update no results message
updateNoResultsMessage(visibleSections);
}
/**
* Check if a table row matches the search query
*/
function rowMatchesSearch(row) {
if (!state.searchQuery) return true;
// Get all searchable text from the row
const symbol = row.querySelector('.symbol')?.textContent || '';
const filePath = row.querySelector('.file-path')?.textContent || '';
const position = row.querySelector('.position')?.textContent || '';
// Combine all searchable fields
const searchableText = (symbol + ' ' + filePath + ' ' + position).toLowerCase();
return searchableText.includes(state.searchQuery);
}
/**
* Update or create "no results" message
*/
function updateNoResultsMessage(visibleCount) {
const issuesSection = document.querySelector('.issues-section');
let noResults = document.getElementById('no-results');
if (visibleCount === 0) {
if (!noResults && issuesSection) {
noResults = document.createElement('div');
noResults.id = 'no-results';
noResults.className = 'no-results';
noResults.innerHTML = '<p>No issues match your current filters and search.</p>';
issuesSection.appendChild(noResults);
}
} else {
if (noResults) {
noResults.remove();
}
}
}
/**
* Open file in IDE (VS Code by default)
*/
window.openInIDE = function(filePath, line, col) {
// Construct absolute path from current working directory
// If path doesn't start with /, it's relative - need to make it absolute
let absolutePath = filePath;
// If the path is relative, we need to construct the absolute path
// The working directory should be stored in a data attribute on the body
if (!filePath.startsWith('/')) {
const cwd = document.body.getAttribute('data-cwd') || '';
if (cwd) {
absolutePath = cwd + '/' + filePath;
}
}
// Build VS Code URL without encoding the entire path
// Format: vscode://file/absolute/path/to/file:line:col
let vscodeUrl = 'vscode://file' + absolutePath;
if (line) vscodeUrl += ':' + line;
if (col) vscodeUrl += ':' + col;
window.location.href = vscodeUrl;
// Fallback message
setTimeout(() => {
console.log('Opening: ' + vscodeUrl);
console.log('If file did not open, install VS Code or configure your IDE protocol handler');
}, 500);
};
})();
</script>
`;
}