mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-26 16:27:20 +00:00
fix: make table rows keyboard-accessible with delegated event listeners (fixes #9)
- Replace inline onclick on <tr> elements with data-action/data-value attributes - Add tabindex="0" and role="row" to all clickable rows - Add delegated click and keydown (Enter/Space) listeners on containers - Remove window._pktSelect, _pktToggleGroup, _pktSelectHash, _nodeSelect globals - Convert to local functions referenced by delegated handlers Affected files: packets.js, nodes.js, analytics.js (channels.js and observers.js had no interactive <tr> elements)
This commit is contained in:
@@ -88,6 +88,20 @@
|
||||
renderTab(btn.dataset.tab);
|
||||
});
|
||||
|
||||
// Delegated click/keyboard handler for clickable table rows
|
||||
const analyticsContent = document.getElementById('analyticsContent');
|
||||
if (analyticsContent) {
|
||||
const handler = (e) => {
|
||||
const row = e.target.closest('tr[data-action="navigate"]');
|
||||
if (!row) return;
|
||||
if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return;
|
||||
if (e.type === 'keydown') e.preventDefault();
|
||||
location.hash = row.dataset.value;
|
||||
};
|
||||
analyticsContent.addEventListener('click', handler);
|
||||
analyticsContent.addEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
try {
|
||||
window._analyticsData = {};
|
||||
const [hashData, rfData, topoData, chanData] = await Promise.all([
|
||||
@@ -568,7 +582,7 @@
|
||||
<table class="analytics-table">
|
||||
<thead><tr><th>Channel</th><th>Hash</th><th>Messages</th><th>Unique Senders</th><th>Last Activity</th><th>Decrypted</th></tr></thead>
|
||||
<tbody>
|
||||
${ch.channels.map(c => `<tr class="clickable-row" onclick="location.hash='#/channels?ch=${c.hash}'">
|
||||
${ch.channels.map(c => `<tr class="clickable-row" data-action="navigate" data-value="#/channels?ch=${c.hash}" tabindex="0" role="row">
|
||||
<td><strong>${esc(c.name || 'Unknown')}</strong></td>
|
||||
<td class="mono">${c.hash}</td>
|
||||
<td>${c.messages}</td>
|
||||
@@ -679,7 +693,7 @@
|
||||
<table class="analytics-table">
|
||||
<thead><tr><th>Node</th><th>Hash Size</th><th>Adverts</th><th>Last Seen</th></tr></thead>
|
||||
<tbody>
|
||||
${data.multiByteNodes.map(n => `<tr class="clickable-row" onclick="location.hash='#/nodes/${n.pubkey ? encodeURIComponent(n.pubkey) : ''}'">
|
||||
${data.multiByteNodes.map(n => `<tr class="clickable-row" data-action="navigate" data-value="#/nodes/${n.pubkey ? encodeURIComponent(n.pubkey) : ''}" tabindex="0" role="row">
|
||||
<td><strong>${esc(n.name)}</strong></td>
|
||||
<td><span class="badge badge-hash-${n.hashSize}">${n.hashSize}-byte</span></td>
|
||||
<td>${n.packets}</td>
|
||||
@@ -697,7 +711,7 @@
|
||||
<tbody>
|
||||
${data.topHops.map(h => {
|
||||
const link = h.pubkey ? `#/nodes/${encodeURIComponent(h.pubkey)}` : `#/packets?search=${h.hex}`;
|
||||
return `<tr class="clickable-row" onclick="location.hash='${link}'">
|
||||
return `<tr class="clickable-row" data-action="navigate" data-value="${link}" tabindex="0" role="row">
|
||||
<td class="mono">${h.hex}</td>
|
||||
<td>${h.name ? `<strong>${esc(h.name)}</strong>` : '<span class="text-muted">unknown</span>'}</td>
|
||||
<td><span class="badge badge-hash-${h.size}">${h.size}-byte</span></td>
|
||||
|
||||
@@ -257,6 +257,20 @@
|
||||
th.addEventListener('click', () => { sortBy = th.dataset.sort; loadNodes(); });
|
||||
});
|
||||
|
||||
// Delegated click/keyboard handler for table rows
|
||||
const tbody = document.getElementById('nodesBody');
|
||||
if (tbody) {
|
||||
const handler = (e) => {
|
||||
const row = e.target.closest('tr[data-action="select"]');
|
||||
if (!row) return;
|
||||
if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return;
|
||||
if (e.type === 'keydown') e.preventDefault();
|
||||
selectNode(row.dataset.value);
|
||||
};
|
||||
tbody.addEventListener('click', handler);
|
||||
tbody.addEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
renderRows();
|
||||
}
|
||||
|
||||
@@ -271,7 +285,7 @@
|
||||
|
||||
tbody.innerHTML = nodes.map(n => {
|
||||
const roleColor = ROLE_COLORS[n.role] || '#6b7280';
|
||||
return `<tr data-key="${n.public_key}" onclick="window._nodeSelect('${n.public_key}')" class="${selectedKey === n.public_key ? 'selected' : ''}">
|
||||
return `<tr data-key="${n.public_key}" data-action="select" data-value="${n.public_key}" tabindex="0" role="row" class="${selectedKey === n.public_key ? 'selected' : ''}">
|
||||
<td>${favStar(n.public_key, 'node-fav')}<strong>${n.name || '(unnamed)'}</strong></td>
|
||||
<td class="mono">${truncate(n.public_key, 16)}</td>
|
||||
<td><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span></td>
|
||||
@@ -407,7 +421,5 @@
|
||||
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
||||
}
|
||||
|
||||
window._nodeSelect = selectNode;
|
||||
|
||||
registerPage('nodes', { init, destroy });
|
||||
})();
|
||||
|
||||
@@ -290,6 +290,24 @@
|
||||
}, 250));
|
||||
fNode.addEventListener('blur', () => { setTimeout(() => fNodeDrop.classList.add('hidden'), 200); });
|
||||
|
||||
// Delegated click/keyboard handler for table rows
|
||||
const pktBody = document.getElementById('pktBody');
|
||||
if (pktBody) {
|
||||
const handler = (e) => {
|
||||
const row = e.target.closest('tr[data-action]');
|
||||
if (!row) return;
|
||||
if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return;
|
||||
if (e.type === 'keydown') e.preventDefault();
|
||||
const action = row.dataset.action;
|
||||
const value = row.dataset.value;
|
||||
if (action === 'select') selectPacket(Number(value));
|
||||
else if (action === 'select-hash') pktSelectHash(value);
|
||||
else if (action === 'toggle-select') { pktToggleGroup(value); pktSelectHash(value); }
|
||||
};
|
||||
pktBody.addEventListener('click', handler);
|
||||
pktBody.addEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
renderTableRows();
|
||||
makeColumnsResizable('#pktTable', 'meshcore-pkt-col-widths');
|
||||
}
|
||||
@@ -310,10 +328,7 @@
|
||||
const groupTypeClass = payloadTypeColor(p.payload_type);
|
||||
const groupSize = p.raw_hex ? Math.floor(p.raw_hex.length / 2) : 0;
|
||||
const isSingle = p.count <= 1;
|
||||
const rowClick = isSingle
|
||||
? `window._pktSelectHash('${p.hash}')`
|
||||
: `window._pktToggleGroup('${p.hash}'); window._pktSelectHash('${p.hash}')`;
|
||||
html += `<tr class="${isSingle ? '' : 'group-header'} ${isExpanded ? 'expanded' : ''}" data-hash="${p.hash}" onclick="${rowClick}">
|
||||
html += `<tr class="${isSingle ? '' : 'group-header'} ${isExpanded ? 'expanded' : ''}" data-hash="${p.hash}" data-action="${isSingle ? 'select-hash' : 'toggle-select'}" data-value="${p.hash}" tabindex="0" role="row">
|
||||
<td style="width:28px;text-align:center;cursor:pointer">${isSingle ? '' : (isExpanded ? '▼' : '▶')}</td>
|
||||
<td>${groupRegion ? `<span class="badge-region">${groupRegion}</span>` : '—'}</td>
|
||||
<td>${timeAgo(p.latest)}</td>
|
||||
@@ -335,7 +350,7 @@
|
||||
let childPath = [];
|
||||
try { childPath = JSON.parse(c.path_json || '[]'); } catch {}
|
||||
const childPathStr = renderPath(childPath);
|
||||
html += `<tr class="group-child" data-id="${c.id}" onclick="window._pktSelect(${c.id})">
|
||||
html += `<tr class="group-child" data-id="${c.id}" data-action="select" data-value="${c.id}" tabindex="0" role="row">
|
||||
<td></td><td>${childRegion ? `<span class="badge-region">${childRegion}</span>` : '—'}</td>
|
||||
<td>${timeAgo(c.timestamp)}</td>
|
||||
<td class="mono">${truncate(c.hash || '', 8)}</td>
|
||||
@@ -365,7 +380,7 @@
|
||||
const pathStr = renderPath(pathHops);
|
||||
const detail = getDetailPreview(decoded);
|
||||
|
||||
return `<tr data-id="${p.id}" onclick="window._pktSelect(${p.id})" class="${selectedId === p.id ? 'selected' : ''}">
|
||||
return `<tr data-id="${p.id}" data-action="select" data-value="${p.id}" tabindex="0" role="row" class="${selectedId === p.id ? 'selected' : ''}">
|
||||
<td></td><td>${region ? `<span class="badge-region">${region}</span>` : '—'}</td>
|
||||
<td>${timeAgo(p.timestamp)}</td>
|
||||
<td class="mono">${truncate(p.hash || String(p.id), 8)}</td>
|
||||
@@ -741,8 +756,7 @@
|
||||
})();
|
||||
|
||||
// Global handlers
|
||||
window._pktSelect = selectPacket;
|
||||
window._pktToggleGroup = async (hash) => {
|
||||
async function pktToggleGroup(hash) {
|
||||
if (expandedHashes.has(hash)) {
|
||||
expandedHashes.delete(hash);
|
||||
renderTableRows();
|
||||
@@ -763,14 +777,14 @@
|
||||
expandedHashes.add(hash);
|
||||
renderTableRows();
|
||||
} catch {}
|
||||
};
|
||||
window._pktSelectHash = async (hash) => {
|
||||
}
|
||||
async function pktSelectHash(hash) {
|
||||
// When grouped, find first packet with this hash
|
||||
try {
|
||||
const data = await api(`/packets?hash=${hash}&limit=1`);
|
||||
if (data.packets?.[0]) selectPacket(data.packets[0].id);
|
||||
} catch {}
|
||||
};
|
||||
}
|
||||
window._pktRefresh = loadPackets;
|
||||
window._pktBYOP = showBYOP;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user