import { sortViolations, filterViolations, parseFailureSummary, escapeHtml } from './logic.js';
document.addEventListener('DOMContentLoaded', () => {
const ui = {
form: document.getElementById('test-form'),
urlInput: document.getElementById('url-input'),
historyToggleButton: document.getElementById('history-toggle-button'),
urlHistoryDropdown: document.getElementById('url-history-dropdown'),
wcagLevelSelect: document.getElementById('wcag-level'),
controls: document.getElementById('controls'),
newTestButton: document.getElementById('new-test-button'),
criticalityFilter: document.getElementById('criticality-filter'),
resultsContainer: document.getElementById('results-container'),
resultsTable: document.getElementById('results-table'),
resultsTableBody: document.getElementById('results-table').querySelector('tbody'),
errorContainer: document.getElementById('error-container'),
errorMessage: document.getElementById('error-message'),
loadingIndicator: document.getElementById('loading'),
};
const state = {
currentResults: null,
currentSort: { column: 'impact', direction: 'asc' },
urlHistory: [],
};
function initialize() {
loadUrlHistory();
focusAndPositionCursor(ui.urlInput);
setupEventListeners();
}
function setupEventListeners() {
ui.form.addEventListener('submit', handleFormSubmit);
ui.newTestButton.addEventListener('click', handleNewTestClick);
ui.criticalityFilter.addEventListener('change', handleCriticalityFilterChange);
ui.urlInput.addEventListener('input', () => {
renderHistoryDropdown(ui.urlInput.value);
ui.urlHistoryDropdown.classList.toggle('hidden', ui.urlHistoryDropdown.children.length === 0);
});
ui.historyToggleButton.addEventListener('click', (e) => {
e.stopPropagation();
const isHidden = ui.urlHistoryDropdown.classList.toggle('hidden');
if (!isHidden) {
renderHistoryDropdown(ui.urlInput.value);
}
});
document.addEventListener('click', (e) => {
if (e.target !== ui.urlInput && !ui.historyToggleButton.contains(e.target)) {
ui.urlHistoryDropdown.classList.add('hidden');
}
});
ui.resultsTable.querySelectorAll('th[data-sort]').forEach(header => {
header.addEventListener('click', () => handleSortClick(header));
});
}
// --- History Management ---
function loadUrlHistory() {
const history = localStorage.getItem('urlHistory');
if (history) {
state.urlHistory = JSON.parse(history);
renderHistoryDropdown();
}
}
function renderHistoryDropdown(filter = '') {
const filteredHistory = state.urlHistory.filter(url => url.toLowerCase().includes(filter.toLowerCase()));
ui.urlHistoryDropdown.innerHTML = '';
filteredHistory.forEach(url => {
const item = document.createElement('div');
item.textContent = url;
item.addEventListener('mousedown', (e) => {
e.preventDefault();
ui.urlInput.value = url;
ui.urlHistoryDropdown.classList.add('hidden');
});
ui.urlHistoryDropdown.appendChild(item);
});
}
function addUrlToHistory(url) {
if (!state.urlHistory.includes(url)) {
state.urlHistory.unshift(url);
if (state.urlHistory.length > 20) {
state.urlHistory.pop();
}
localStorage.setItem('urlHistory', JSON.stringify(state.urlHistory));
}
}
// --- UI State and View Updates ---
function setViewState(view) {
ui.form.classList.toggle('hidden', view !== 'form');
ui.controls.classList.toggle('hidden', view !== 'results');
ui.resultsContainer.classList.toggle('hidden', view !== 'results');
ui.loadingIndicator.classList.toggle('hidden', view !== 'loading');
}
function showError(message) {
ui.errorMessage.textContent = message;
ui.errorContainer.classList.remove('hidden');
}
function hideError() {
ui.errorContainer.classList.add('hidden');
}
function focusAndPositionCursor(inputElement) {
inputElement.focus();
setTimeout(() => {
const length = inputElement.value.length;
inputElement.setSelectionRange(length, length);
}, 0);
}
// --- Event Handlers ---
async function handleFormSubmit(event) {
event.preventDefault();
ui.urlHistoryDropdown.classList.add('hidden');
const url = ui.urlInput.value.trim();
try {
const parsedUrl = new URL(url);
if (!parsedUrl.hostname.includes('.')) {
throw new Error('Invalid hostname.');
}
} catch (e) {
showError('Please enter a valid and complete URL (e.g., https://www.example.com)');
return;
}
const wcagLevel = ui.wcagLevelSelect.value;
const selectedCriticalities = getSelectedCriticalities();
hideError();
setViewState('loading');
try {
const response = await fetch('/api/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
wcagLevel,
criticality: selectedCriticalities.includes('all') ? [] : selectedCriticalities
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'An unknown error occurred.');
}
state.currentResults = await response.json();
addUrlToHistory(url);
refreshResultsView();
setViewState('results');
} catch (error) {
showError(error.message);
setViewState('form');
}
}
function handleNewTestClick() {
setViewState('form');
focusAndPositionCursor(ui.urlInput);
}
function handleCriticalityFilterChange() {
const allCheckbox = ui.criticalityFilter.querySelector('input[value="all"]');
const otherCheckboxes = ui.criticalityFilter.querySelectorAll('input:not([value="all"])');
if (event.target.value === 'all' && event.target.checked) {
otherCheckboxes.forEach(cb => cb.checked = false);
} else if (event.target.value !== 'all' && event.target.checked) {
allCheckbox.checked = false;
}
const anyChecked = Array.from(otherCheckboxes).some(cb => cb.checked);
if (!anyChecked) {
allCheckbox.checked = true;
}
if (!ui.resultsContainer.classList.contains('hidden')) {
refreshResultsView();
}
}
function handleSortClick(header) {
const column = header.dataset.sort;
const direction = (state.currentSort.column === column && state.currentSort.direction === 'asc') ? 'desc' : 'asc';
state.currentSort = { column, direction };
refreshResultsView();
}
// --- Results Processing and Rendering ---
function refreshResultsView() {
if (!state.currentResults) return;
const selectedCriticalities = getSelectedCriticalities();
const filtered = filterViolations(state.currentResults.violations, selectedCriticalities);
const sorted = sortViolations(filtered, state.currentSort);
renderResultsTable(sorted, state.currentResults.url);
updateSortIndicators();
}
function getSelectedCriticalities() {
return Array.from(ui.criticalityFilter.querySelectorAll('input:checked')).map(cb => cb.value);
}
function renderResultsTable(violations, pageUrl) {
ui.resultsTableBody.innerHTML = '';
if (violations.length === 0) {
const row = ui.resultsTableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 6;
cell.textContent = 'No accessibility violations match the current filter.';
cell.style.textAlign = 'center';
return;
}
const rowsHtml = violations.map(violation => createViolationRowHtml(violation, pageUrl)).join('');
ui.resultsTableBody.innerHTML = rowsHtml;
}
function createViolationRowHtml(violation, pageUrl) {
const { id, description, helpUrl, impact, nodes } = violation;
const elementHtml = nodes[0]?.html || 'N/A';
const failureSummary = nodes[0]?.failureSummary || '';
const suggestionsHtml = parseFailureSummary(failureSummary);
return `
<tr>
<td data-label="Type">${id}</td>
<td data-label="Description">${description} <a href="${helpUrl}" target="_blank">Learn More</a></td>
<td data-label="Criticality" class="criticality-${impact}">${impact}</td>
<td data-label="Element"><code>${escapeHtml(elementHtml)}</code></td>
<td data-label="URL">${pageUrl}</td>
<td data-label="Suggested Actions">${suggestionsHtml}</td>
</tr>
`;
}
function updateSortIndicators() {
ui.resultsTable.querySelectorAll('th[data-sort]').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sort === state.currentSort.column) {
th.classList.add(`sort-${state.currentSort.direction}`);
}
});
}
initialize();
});