mirror of
https://github.com/lockfale/OSINT-Framework.git
synced 2026-08-29 03:09:42 +02:00
feat(ui): add client-side search for tools (THE-66)
Adds a debounced text search input above the D3 tree that filters all ~1,000+ tool nodes by name and description. Results appear as a flat list with breadcrumb paths; clearing the input hides the panel. - No build step required: vanilla JS, no new dependencies - Sanitizes HTML output (escapeHtml/escapeAttr) and validates URLs (http/https only) - Dark and light mode compatible via existing CSS variables - Results capped at 50 to keep rendering fast Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -105,3 +105,75 @@ h3 {
|
||||
a {
|
||||
color: var(--color-node-stroke);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
#search-container {
|
||||
text-align: center;
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-family: "Helvetica Neue", Helvetica;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-link);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#search-input:focus {
|
||||
border-color: var(--color-node-stroke);
|
||||
}
|
||||
|
||||
#search-results {
|
||||
margin: 8px auto 0;
|
||||
width: 600px;
|
||||
max-width: 90vw;
|
||||
text-align: left;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-link);
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#search-results.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--color-link);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item a {
|
||||
font-weight: bold;
|
||||
color: var(--color-node-stroke);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.search-result-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.search-result-path {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.search-no-results {
|
||||
padding: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
OSINT Framework
|
||||
<hr/>
|
||||
</div>
|
||||
<div id="search-container">
|
||||
<input type="text" id="search-input" placeholder="Search tools..." autocomplete="off" aria-label="Search OSINT tools">
|
||||
<div id="search-results" role="listbox" aria-label="Search results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/arf.js"></script>
|
||||
|
||||
+73
-1
@@ -3,7 +3,8 @@ var margin = [20, 120, 20, 140],
|
||||
height = 800 - margin[0] - margin[2],
|
||||
i = 0,
|
||||
duration = 1250,
|
||||
root;
|
||||
root,
|
||||
allSearchNodes = [];
|
||||
|
||||
var tree = d3.tree()
|
||||
.size([height, width]);
|
||||
@@ -29,6 +30,11 @@ d3.json("arf.json").then(function(json) {
|
||||
root.x0 = height / 2;
|
||||
root.y0 = 0;
|
||||
|
||||
// Collect all tool nodes (those with URLs) for search
|
||||
allSearchNodes = root.descendants().filter(function(d) {
|
||||
return d.data.url;
|
||||
});
|
||||
|
||||
function collapse(d) {
|
||||
if (d.children) {
|
||||
d._children = d.children;
|
||||
@@ -39,6 +45,7 @@ d3.json("arf.json").then(function(json) {
|
||||
|
||||
root.children.forEach(collapse);
|
||||
update(root);
|
||||
initSearch();
|
||||
});
|
||||
|
||||
function update(source) {
|
||||
@@ -160,6 +167,71 @@ function toggle(d) {
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side search over all tool nodes.
|
||||
var searchDebounceTimer = null;
|
||||
|
||||
function initSearch() {
|
||||
var input = document.getElementById("search-input");
|
||||
if (!input) return;
|
||||
input.addEventListener("input", function() {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
var query = input.value.trim();
|
||||
searchDebounceTimer = setTimeout(function() { doSearch(query); }, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function doSearch(query) {
|
||||
var results = document.getElementById("search-results");
|
||||
if (!results) return;
|
||||
|
||||
if (!query) {
|
||||
results.innerHTML = "";
|
||||
results.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
var lower = query.toLowerCase();
|
||||
var matches = allSearchNodes.filter(function(d) {
|
||||
var name = (d.data.name || "").toLowerCase();
|
||||
var desc = (d.data.description || "").toLowerCase();
|
||||
return name.indexOf(lower) !== -1 || desc.indexOf(lower) !== -1;
|
||||
}).slice(0, 50);
|
||||
|
||||
results.classList.add("visible");
|
||||
|
||||
if (matches.length === 0) {
|
||||
results.innerHTML = '<div class="search-no-results">No results found for \u201c' + escapeHtml(query) + '\u201d</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = matches.map(function(d) {
|
||||
var path = d.ancestors().reverse().slice(1, -1).map(function(a) {
|
||||
return escapeHtml(a.data.name);
|
||||
}).join(" \u203a ");
|
||||
var name = escapeHtml(d.data.name);
|
||||
var url = safeUrl(d.data.url);
|
||||
return '<div class="search-result-item">' +
|
||||
'<a href="' + escapeAttr(url) + '" target="_blank" rel="noopener noreferrer">' + name + '</a>' +
|
||||
(path ? '<div class="search-result-path">' + path + '</div>' : '') +
|
||||
'</div>';
|
||||
}).join("");
|
||||
|
||||
results.innerHTML = html;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return (str || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
return (str || "").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function safeUrl(url) {
|
||||
if (!url) return "#";
|
||||
return /^https?:\/\//i.test(url) ? url : "#";
|
||||
}
|
||||
|
||||
// Toggle light/dark mode and persist preference.
|
||||
function goDark() {
|
||||
var body = document.body;
|
||||
|
||||
Reference in New Issue
Block a user