This commit is contained in:
Radon 2025-01-21 14:32:54 -06:00
parent ef29695427
commit 7fae9df5b4
4 changed files with 361 additions and 54 deletions

View File

@ -42,6 +42,103 @@
--input-button-active-fg: #dce0e8;
}
.search-trigger {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.search-container {
position: flex;
display: flex;
align-items: center;
background: var(--pum-button-active-bg);
border-radius: 24px;
transition: width 0.2s ease;
overflow: hidden;
width: 48px;
height: 48px;
}
.search-container.expanded {
width: 300px;
}
.search-button {
background: none;
border: none;
padding: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--pum-button-active-bg);
color: var(--pum-button-inactive-fg);
}
.search-button:hover {
background: var(--pum-button-active-bg);
color: var(--pum-button-active-fg);
}
.search-input {
display: none;
flex: 2;
border: none;
padding: 8px;
margin-left: 4px;
outline: none;
font-size: 14px;
}
.search-container.expanded .search-input {
display: block;
}
.search-highlight {
background-color: yellow;
color: gray;
}
.search-highlight.current {
background-color: darkorange;
color: black;
}
/* Search icon using CSS */
.search-icon {
width: 15px;
height: 15px;
border: 2px solid var(--pum-button-inactive-fg);
border-radius: 50%;
position: relative;
}
.search-icon::after {
content: '';
position: absolute;
width: 2px;
height: 10px;
background: var(--pum-button-inactive-fg);
bottom: -8px;
right: -3px;
transform: rotate(-45deg);
}
.search-count {
display: none;
margin-left: 8px;
font-size: 12px;
color: var(--pum-title-color);
}
.search-container.expanded .search-count {
display: block;
}
body {
font-family: Arial, sans-serif;
margin: 0;
@ -356,6 +453,9 @@ button.scroll:hover {
max-width: 100%;
}
.search-container {
display: none;
}
.radchat {
display: none;
}
@ -451,6 +551,9 @@ button.scroll:hover {
/* Small mobile devices */
@media screen and (max-width: 380px) {
.search-container {
display: none;
}
.radchat {
display: none;
}

View File

@ -67,6 +67,20 @@
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
</svg></button>
</div>
<div class="search-trigger">
<div class="search-container" id="searchContainer">
<button class="search-button" id="searchButton">
<div class="search-icon"></div>
</button>
<input
type="text"
class="search-input"
id="searchInput"
placeholder="Type to search page..."
>
<span class="search-count" id="searchCount"></span>
</div>
</div>
<div id="status"></div>
</div>

View File

@ -495,6 +495,195 @@ async function initialize() {
setInterval(loadUsers, 1000);
setInterval(pingCheck, 3000);
await loadMessages(true);
initializeSearchBox();
}
function initializeSearchBox() {
const searchContainer = document.getElementById("searchContainer");
const searchButton = document.getElementById("searchButton");
const searchInput = document.getElementById("searchInput");
const searchCount = document.getElementById("searchCount");
let currentMatchIndex = -1;
let matches = [];
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function clearHighlights() {
// Remove all existing highlights
const highlights = document.querySelectorAll(
".search-highlight",
);
highlights.forEach((highlight) => {
const parent = highlight.parentNode;
parent.replaceChild(
document.createTextNode(highlight.textContent),
highlight,
);
parent.normalize();
});
matches = [];
currentMatchIndex = -1;
searchCount.textContent = "";
}
function findTextNodes(element, textNodes = []) {
// Skip certain elements
if (element.nodeType === Node.ELEMENT_NODE) { // Check if it's an element node first
if (
element.tagName === "SCRIPT" ||
element.tagName === "STYLE" ||
element.tagName === "NOSCRIPT" ||
(element.classList &&
element.classList.contains(
"search-container",
)) ||
(element.classList &&
element.classList.contains(
"search-highlight",
))
) {
return textNodes;
}
}
// Check if this node is a text node with non-whitespace content
if (
element.nodeType === Node.TEXT_NODE &&
element.textContent.trim()
) {
textNodes.push(element);
}
// Recursively check all child nodes
const children = element.childNodes;
for (let i = 0; i < children.length; i++) {
findTextNodes(children[i], textNodes);
}
return textNodes;
}
function findAndHighlight(searchText) {
if (!searchText) {
clearHighlights();
return;
}
clearHighlights();
const searchRegex = new RegExp(escapeRegExp(searchText), "gi");
const textNodes = findTextNodes(document.body);
textNodes.forEach((node) => {
const matches = [
...node.textContent.matchAll(searchRegex),
];
if (matches.length > 0) {
const span = document.createElement("span");
span.innerHTML = node.textContent.replace(
searchRegex,
(match) =>
`<span class="search-highlight">${match}</span>`,
);
node.parentNode.replaceChild(span, node);
}
});
// Collect all highlights
matches = Array.from(
document.querySelectorAll(".search-highlight"),
);
if (matches.length > 0) {
currentMatchIndex = 0;
matches[0].classList.add("current");
matches[0].scrollIntoView({
behavior: "smooth",
block: "center",
});
}
// Update count
searchCount.textContent = matches.length > 0
? `${currentMatchIndex + 1}/${matches.length}`
: "No matches";
}
function nextMatch() {
if (matches.length === 0) return;
matches[currentMatchIndex].classList.remove("current");
currentMatchIndex = (currentMatchIndex + 1) % matches.length;
matches[currentMatchIndex].classList.add("current");
matches[currentMatchIndex].scrollIntoView({
behavior: "smooth",
block: "center",
});
searchCount.textContent = `${
currentMatchIndex + 1
}/${matches.length}`;
}
searchButton.addEventListener("click", () => {
if (!searchContainer.classList.contains("expanded")) {
searchContainer.classList.add("expanded");
searchInput.focus();
} else {
nextMatch();
}
});
searchInput.addEventListener("input", (e) => {
findAndHighlight(e.target.value.trim());
});
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
nextMatch();
} else if (e.key === "Escape") {
searchContainer.classList.remove("expanded");
searchInput.value = "";
clearHighlights();
}
});
// Debug function to check search coverage (call from console: checkSearchCoverage())
window.checkSearchCoverage = function () {
const textNodes = findTextNodes(document.body);
console.log(
"Total searchable text nodes found:",
textNodes.length,
);
textNodes.forEach((node, i) => {
console.log(`Node ${i + 1}:`, {
text: node.textContent,
parent: node.parentElement.tagName,
path: getNodePath(node),
});
});
};
function getNodePath(node) {
const path = [];
while (node && node.parentElement) {
let index = Array.from(node.parentElement.childNodes)
.indexOf(node);
path.unshift(`${node.parentElement.tagName}[${index}]`);
node = node.parentElement;
}
return path.join(" > ");
}
// Close search when clicking outside
document.addEventListener("click", (e) => {
if (!searchContainer.contains(e.target)) {
searchContainer.classList.remove("expanded");
searchInput.value = "";
clearHighlights();
}
});
}
initialize();

View File

@ -5,11 +5,12 @@
### Mid Priority
- Other embeds (Twitter posts, spotify tracks, soundcloud, github repos, instagram posts, other video platforms)
### Low Priority
- Nothing yet
- Reposition the search button
- Fix mobile views instead of hiding elements that you don't want to position properly
## Backend
### High Priority
- Lazy load with pagination (frontend and backend)
### Mid Priority
- Nothing yet
### Low Priority
- Search functionality?
- Nothing yet