Skip to content

Commit

Permalink
feat(sidebar): implement SidebarFilterer with highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
caugner committed May 29, 2023
1 parent 530ddcf commit 432d41d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 57 deletions.
153 changes: 153 additions & 0 deletions client/src/document/organisms/sidebar/SidebarFilterer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { splitQuery } from "../../../utils";

export class SidebarFilterer {
applyFilter(query: string) {
if (query) {
this.showOnlyMatchingItems(query);
} else {
this.showAllItems();
}
}
root: HTMLElement;

constructor(root: HTMLElement) {
this.root = root;
}

private get parents(): HTMLDetailsElement[] {
return Array.from(
this.root.querySelectorAll<HTMLDetailsElement>("details")
);
}

private get links(): HTMLAnchorElement[] {
return Array.from(this.root.querySelectorAll<HTMLAnchorElement>("a[href]"));
}

showAllItems() {
this.links.forEach((link) => this.resetLink(link));
this.parents.forEach((parent) => this.resetParent(parent));
}

private resetLink(link: HTMLAnchorElement) {
this.resetHighlighting(link);
const target = link.closest("li") || link;
target.style.display = "inherit";
}

private resetParent(detail: HTMLDetailsElement) {
detail.style.display = "inherit";
if (detail.dataset.open) {
detail.open = JSON.parse(detail.dataset.open);
delete detail.dataset.open;
}
}

private resetHighlighting(link: HTMLAnchorElement) {
const nodes = Array.from(link.querySelectorAll<HTMLElement>("span, mark"));
const parents = new Set<HTMLElement>();
nodes.forEach((node) => {
const parent = node.parentElement;
node.replaceWith(document.createTextNode(node.textContent ?? ""));
if (parent) {
parents.add(parent);
}
});
parents.forEach((parent) => parent.normalize());
}

showOnlyMatchingItems(query: string) {
this.parents.forEach(this.collapseDetail);

// Show/hide items (+ show parents).
const terms = splitQuery(query);
this.links.forEach((link) => {
this.resetHighlighting(link);
const haystack = link.innerText.toLowerCase();
const isMatch = terms.every((needle) => haystack.includes(needle));

const target = link.closest("li") || link;
target.style.display = isMatch ? "inherit" : "none";

if (isMatch) {
this.highlightMatches(link, terms);
this.expandParents(target);
}
});
}

private collapseDetail(el: HTMLDetailsElement) {
el.style.display = "none";
el.dataset.open = el.dataset.open ?? String(el.open);
el.open = false;
}

private highlightMatches(el: HTMLElement, terms: string[]) {
const nodes = Array.from(el.childNodes).filter(
(node) => node.nodeType === Node.TEXT_NODE
) as (Element & Text)[];

nodes.forEach((node) => {
const haystack = node.textContent?.toLowerCase();
if (!haystack) {
return;
}

const ranges = new Map<number, number>();
terms.forEach((needle) => {
const index = haystack.indexOf(needle);
if (index !== -1) {
ranges.set(index, index + needle.length);
}
});
const sortedRanges = Array.from(ranges.entries()).sort(
([x1, y1], [x2, y2]) => x1 - x2 || y1 - y2
);

let rest = node;
let cursor = 0;

for (const [rangeBegin, rangeEnd] of sortedRanges) {
if (rangeBegin < cursor) {
// Just ignore conflicting range.
continue;
}

// Split.
const match = rest.splitText(rangeBegin - cursor);
const newRest = match.splitText(rangeEnd - rangeBegin);

// Convert text nodes to HTML elements.
this.replaceChildNode(rest, "span");
this.replaceChildNode(match, "mark");

rest = newRest as Element & Text;
cursor = rangeEnd;
}

this.replaceChildNode(rest, "span");
});
}

private replaceChildNode(node: ChildNode, tagName: string) {
const text = node.textContent;
if (text) {
const currentSpan = document.createElement(tagName);
currentSpan.innerText = text;
node.replaceWith(currentSpan);
} else {
node.parentElement?.removeChild(node);
}
}

private expandParents(target: HTMLElement) {
let parent = target.parentElement?.closest("details");
while (parent) {
if (parent instanceof HTMLDetailsElement) {
parent.style.display = "inherit";
parent.open = true;
}
parent = parent.parentElement?.closest("details");
}
}
}
61 changes: 4 additions & 57 deletions client/src/document/organisms/sidebar/filter.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,18 @@
import { useEffect, useState } from "react";
import { splitQuery } from "../../../utils";
import { SidebarFilterer } from "./SidebarFilterer";

export function SidebarFilter() {
const [query, setQuery] = useState("");

useEffect(() => {
const quicklinks = document.getElementById("sidebar-quicklinks");

if (!quicklinks) {
return;
}

const details = Array.from(
quicklinks.querySelectorAll<HTMLDetailsElement>("details")
);
const links = Array.from(
quicklinks.querySelectorAll<HTMLAnchorElement>("a[href]")
);

if (query) {
// Hide and collapse all parents.
details.forEach((detail) => {
detail.style.display = "none";
detail.dataset.open = detail.dataset.open ?? String(detail.open);
detail.open = false;
});

// Show/hide items (+ show parents).
const q = splitQuery(query);
links.forEach((link) => {
const innerTextLC = link.innerText.toLowerCase();
const isMatch = q.every((q) => innerTextLC.includes(q));
const target = link.closest("li") || link;

if (isMatch) {
// Show item.
target.style.display = "inherit";

// Expand parents.
let parent = target.parentElement;
while (parent) {
if (parent instanceof HTMLDetailsElement) {
parent.style.display = "inherit";
parent.open = true;
}
parent = parent.parentElement;
}
} else {
// Hide item.
target.style.display = "none";
}
});
} else {
// Show all links.
links.forEach((link) => {
const target = link.closest("li") || link;
target.style.display = "inherit";
});

// Show all parents.
details.forEach((detail) => {
detail.style.display = "inherit";
if (detail.dataset.open) {
detail.open = JSON.parse(detail.dataset.open);
delete detail.dataset.open;
}
});
}
const filterer = new SidebarFilterer(quicklinks);
filterer.applyFilter(query);
}, [query]);

return (
Expand Down
1 change: 1 addition & 0 deletions client/src/document/organisms/sidebar/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
display: inline-flex;
hyphens: auto;
padding: 0.25rem;
white-space: pre;

&:hover,
&:focus {
Expand Down

0 comments on commit 432d41d

Please sign in to comment.