Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sidebar): add filter #8968

Merged
merged 123 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from 118 commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
46b1c80
feat(sidebar): add filter field (w/o logic)
caugner May 25, 2023
119c9e8
feat(sidebar): implement filtering
caugner May 29, 2023
9b81921
chore(sidebar): split query like quicksearch
caugner May 29, 2023
98a7538
feat(sidebar): implement SidebarFilterer with highlighting
caugner May 29, 2023
960657d
feat(sidebar): style filter, add icons + clear button
caugner May 30, 2023
17dabf8
fixup! feat(sidebar): style filter, add icons + clear button
caugner May 30, 2023
8f2435e
fix(sidebar): make filter sticky
caugner May 30, 2023
26eb290
feat(sidebar): add thumbs to rate feature
caugner May 30, 2023
db550ef
fix(sidebar): fix button/icon placement
caugner May 30, 2023
3340c8c
fix(sidebar): stop fading out top area
caugner May 30, 2023
4c34a7d
chore(sidebar): refine layout + thumbs
caugner May 30, 2023
7d34401
fix(sidebar): highlight matches in recursive text nodes
caugner May 30, 2023
e2cecec
fixup! fix(sidebar): highlight matches in recursive text nodes
caugner May 30, 2023
b23a70d
fix(sidebar): avoid whitespace: pre by wrapping highlighted text
caugner May 30, 2023
ba8d27c
feat(sidebar): restore scroll position after filtering
caugner May 30, 2023
40b27a7
fix(sidebar): remove filter background
caugner May 30, 2023
af1e76d
fixup! feat(sidebar): restore scroll position after filtering
caugner May 30, 2023
6ab2587
fix(sidebar): avoid filter clear button moving
caugner May 30, 2023
34012bb
fixup! chore(sidebar): refine layout + thumbs
caugner May 31, 2023
e3f255a
fix(sidebar): add backdrop to filter
caugner May 31, 2023
e118e6f
chore(sidebar): move thumbs below filter + add text
caugner May 31, 2023
fdb46f4
chore(thumbs): change how thumbs up/down behave on hover/active
caugner May 31, 2023
02f6c0f
chore(sidebar): fade thumbs unless hover/focus-within
caugner May 31, 2023
99a31f2
fix(sidebar): make toc non-relative
caugner May 31, 2023
2d193d4
fix(sidebar): fix glean-thumbs height
caugner May 31, 2023
2e40f97
fix(thumbs): use 0/1 for rating value
caugner May 31, 2023
6efe597
fix(sidebar): avoid icons showing on top of the filter
caugner May 31, 2023
ec60003
chore(sidebar): hide filter on mobile
caugner May 31, 2023
a2569ef
fixup! fix(sidebar): avoid icons showing on top of the filter
caugner May 31, 2023
551eb13
Revert "chore(sidebar): fade thumbs unless hover/focus-within"
caugner May 31, 2023
a5aa418
chore(sidebar): make filter field span full width
caugner May 31, 2023
6ea75e7
chore(sidebar): align thumbs right
caugner May 31, 2023
192e1e0
chore(sidebar): reduce filter font-size
caugner May 31, 2023
cbcd7b1
fix(sidebar): hide filter on medium screen as well
caugner May 31, 2023
0f7af37
feat(sidebar): record sidebar_click_with_filter event
caugner May 31, 2023
df98cce
chore(sidebar): center thumbs
caugner May 31, 2023
1c9544e
chore(thumbs): rephrase default question
caugner May 31, 2023
9422238
fixup! feat(sidebar): record sidebar_click_with_filter event
caugner May 31, 2023
5da76c3
chore(sidebar): show the filter clear only if there is a value
caugner May 31, 2023
cc33088
feat(sidebar-filter): show number of matches in footer
caugner May 31, 2023
40e9e5e
feat(sidebar-filter): show/hide section headings dynamically
caugner May 31, 2023
7375bfa
chore(sidebar-filter): show match count below thumbs
caugner Jun 1, 2023
2618d2f
chore(thumbs): refine wording
caugner Jun 1, 2023
0a59a41
fix(sidebar-filter): use --mark-color for highlights
caugner Jun 2, 2023
659b559
fix(sidebar-filter): align left width breadcrumbs
caugner Jun 2, 2023
a9b506c
fix(sidebar-filter): hide on pages w/o sidebar
caugner Jun 2, 2023
5fd075f
fix(sidebar-filter): add border on focus
caugner Jun 2, 2023
4679281
fix(sidebar-filter): left align thumbs + items with filter icon
caugner Jun 2, 2023
975e294
fix(sidebar-filter): increase space between input and thumbs
caugner Jun 2, 2023
6b1a99e
chore(sidebar-filter): use different color for match count
caugner Jun 2, 2023
1ba0f55
fix(sidebar-filter): increase backdrop length + distance
caugner Jun 2, 2023
f6a37e9
chore(thumbs): highlight confirmation for 5 seconds, then transition …
caugner Jun 2, 2023
5fc145a
chore(thumbs): add heart emoji
caugner Jun 2, 2023
0623c6e
feat(sidebar-filter): collapse if no focus/value
caugner Jun 2, 2023
27d884a
chore(sidebar-filter): show footer only with focus
caugner Jun 2, 2023
bcc4a0d
fix(sidebar): show footer upon user interaction
caugner Jun 2, 2023
a98d55e
feat(sidebar-filter): measure when users focus the filter
caugner Jun 2, 2023
cb7425f
feat(sidebar-filter): show count inside input field
caugner Jun 2, 2023
82444ba
chore(thumbs): improve animation
caugner Jun 2, 2023
1ad51b0
fix(thumbs): make up/downLabel props
caugner Jun 2, 2023
8306118
fix(sidebar-filter): redistribute responsibilities between .sidebar-a…
caugner Jun 2, 2023
bc611f6
chore(sidebar-filter): keep input open + thumbs visible until cleared
caugner Jun 2, 2023
964d75e
fix(sidebar-filter): optimize gradient/padding to keep sidebar begin …
caugner Jun 2, 2023
42fff01
fix(sidebar-filter): make it available on mobile
caugner Jun 2, 2023
92a7238
fix(sidebar-filter): align items centrally
caugner Jun 2, 2023
4902f19
fix(sidebar): remove padding-top
caugner Jun 2, 2023
f426b1c
fixup! fix(sidebar-filter): optimize gradient/padding to keep sidebar…
caugner Jun 2, 2023
af87710
fix(sidebar-filter): unset color for mark elements
caugner Jun 2, 2023
623bdab
chore(sidebar-filter): do not replace unrelated text nodes
caugner Jun 2, 2023
ab38a70
refactor(sidebar-filter): resolve unnecessary return
caugner Jun 2, 2023
3688a7c
refactor(sidebar-filter): replace while with for..of
caugner Jun 2, 2023
719644b
refactor(sidebar-filter): move function behind constructor
caugner Jun 2, 2023
c76f4c7
refactor(sidebar-filter): use querySelector instead of getElementById
caugner Jun 2, 2023
a71d6aa
chore(sidebar-filter): hide "In this article" if no match
caugner Jun 5, 2023
e7b780f
refactor(sidebar-fiilter): rename collapse{Detail => Parent}
caugner Jun 5, 2023
4a21121
refator(sidebar-filter): rename dataset.{open => wasOpen}
caugner Jun 5, 2023
da4979d
chore: remove unnecessary jsdoc
caugner Jun 5, 2023
4fec9d9
fix(sidebar-filter): exclude + hide TOC
caugner Jun 5, 2023
ad17162
refactor(sidebar-filter): inline SidebarFilterer.root
caugner Jun 5, 2023
7bb2c13
feat(thumbs): hide if previously submitted
caugner Jun 5, 2023
01aea1d
fix(sidebar-filter): remove weird icon margin
caugner Jun 5, 2023
dff58a7
fix(sidebar-filter): apply clear button style to thumbs buttons as well
caugner Jun 5, 2023
d212d75
fixup! feat(thumbs): hide if previously submitted
caugner Jun 5, 2023
6774799
chore(sidebar-filter): align heart with clear button
caugner Jun 5, 2023
65a2f3b
refactor(sidebar-filter): extract toggleElement()
caugner Jun 6, 2023
119277b
fix(sidebar-filter): show by resetting style.display
caugner Jun 6, 2023
7ae26aa
fix(sidebar-filter): avoid forEach without this context
caugner Jun 6, 2023
0de11bf
fix(thumbs): increase gap between icons
caugner Jun 6, 2023
405cd50
fix(sidebar-filter): increase icon size to 1rem/16px
caugner Jun 6, 2023
afebb01
fix(sidebar-filter): use border-radius of 1rem
caugner Jun 6, 2023
230bcb5
fix(sidebar-filter): align thumbs left + add margin-top to content
caugner Jun 6, 2023
add9db5
chore(sidebar-filter): increase font-size
caugner Jun 6, 2023
63482e3
fix(sidebar-filter): do not resize thumbs button
caugner Jun 6, 2023
42b3364
fix(sidebar-filter): add padding-right to avoid touching the scrollbar
caugner Jun 6, 2023
ceda9b1
chore(sidebar-filter): align feedback with filter icon
caugner Jun 6, 2023
03ac5ea
refactor(sidebar-filter): extract getLinkContainer()
caugner Jun 6, 2023
921c8f8
refactor(sidebar-filter): extract getParents()
caugner Jun 6, 2023
d18edb5
refactor(sidebar-filter): extract getHeading[Container]()
caugner Jun 6, 2023
c6a74a2
perf(sidebar-filter): avoid duplicate work
caugner Jun 6, 2023
1cb333d
refactor(sidebar-filter): rename all{Headings,Parents}
caugner Jun 6, 2023
db3747a
refactor(sidebar-filter): merge get{Link,Heading}Container()
caugner Jun 6, 2023
67bcf71
perf(sidebar-filter): reuse SidebarFilterer
caugner Jun 6, 2023
dce2972
fix(sidebar-filter): store scrollTop in ref, not state
caugner Jun 6, 2023
6bdd5f5
refactor(sidebar-filter): extract usePersistedScrollPosition()
caugner Jun 6, 2023
43d8cec
refactor(sidebar-filter): extract useSidebarFilter()
caugner Jun 6, 2023
ad775ea
refactor(sidebar-filter): rename {hasUserInteraction => isActive}
caugner Jun 6, 2023
fd71c3f
chore(sidebar-filter): show count only if active
caugner Jun 6, 2023
f4bb33e
chore(sidebar): add comment about reducing margin-top again
caugner Jun 6, 2023
8badf14
fix(sidebar-filter): avoid margin-top on mobile
caugner Jun 6, 2023
06467cc
fix(sidebar-filter): restore scroll positon on mobile
caugner Jun 6, 2023
3d30de5
fix(sidebar-filter): hide parent container, not parent
caugner Jun 6, 2023
1255bc2
refactor(sidebar-filter): rename target => container
caugner Jun 6, 2023
8758bb3
fix(sidebar-filter): set z-index on sidebar-actions, instead of icon
caugner Jun 6, 2023
28dea13
fix(sidebar-filter): disable autocomplete
caugner Jun 6, 2023
c157fae
fix(sidebar-filter): use textContent, not innerText
caugner Jun 6, 2023
6dd696e
feat(sidebar-filter): color filter icon when we have input
caugner Jun 7, 2023
85d8067
Merge branch 'main' into sidebar-filter
caugner Jun 7, 2023
639407f
fix(sidebar-filter): trim query
caugner Jun 8, 2023
230646f
chore(sidebar): remove unnecessary `position: relative`
caugner Jun 8, 2023
371fc5f
fix(sidebar-filter): assign quicklinks/sidebarInnerNav once
caugner Jun 8, 2023
d6cb3af
chore(sidebar-filter): remove unnecessary CSS block
caugner Jun 8, 2023
1cb822b
refactor(sidebar-filter): rename params for consistency
caugner Jun 8, 2023
e9ce3ac
refactor(sidebar-filter): rename functions for clarity
caugner Jun 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/src/assets/icons/filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions client/src/document/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,7 @@ kbd {
.sidebar {
mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgb(0, 0, 0) 3rem calc(100% - 3rem),
rgb(0, 0, 0) 0% calc(100% - 3rem),
rgba(0, 0, 0, 0) 100%
);
}
Expand Down
258 changes: 258 additions & 0 deletions client/src/document/organisms/sidebar/SidebarFilterer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { splitQuery } from "../../../utils";

export class SidebarFilterer {
allHeadings: HTMLElement[];
allParents: HTMLDetailsElement[];
items: Array<{
haystack: string;
link: HTMLAnchorElement;
container: HTMLElement;
heading: HTMLElement | undefined;
parents: HTMLDetailsElement[];
}>;
toc: HTMLElement | null;

constructor(root: HTMLElement) {
this.allHeadings = Array.from(
root.querySelectorAll<HTMLElement>("li strong")
);
this.allParents = Array.from(
root.querySelectorAll<HTMLDetailsElement>("details")
);

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

this.items = links.map((link) => ({
haystack: (link.textContent ?? "").toLowerCase(),
link,
container: this.getContainer(link),
heading: this.getHeading(link),
parents: this.getParents(link),
}));

this.toc =
root.closest<HTMLElement>(".sidebar")?.querySelector(".in-nav-toc") ??
null;
}

applyFilter(query: string) {
if (query) {
this.toggleTOC(false);
return this.showOnlyMatchingItems(query);
} else {
this.toggleTOC(true);
this.showAllItems();
return undefined;
}
}

private toggleTOC(show: boolean) {
if (this.toc) {
this.toggleElement(this.toc, show);
}
}

private toggleElement(el: HTMLElement, show: boolean) {
el.style.display = show ? "" : "none";
}

showAllItems() {
this.items.forEach(({ link }) => this.resetLink(link));
this.allHeadings.forEach((heading) => this.resetHeading(heading));
this.allParents.forEach((parent) => this.resetParent(parent));
}

private resetLink(link: HTMLAnchorElement) {
this.resetHighlighting(link);
const container = this.getContainer(link);
this.toggleElement(container, true);
}

private getContainer(el: HTMLElement) {
return el.closest("li") || el;
}

private resetHeading(heading: HTMLElement) {
const container = this.getContainer(heading);
this.toggleElement(container, true);
}

private resetParent(detail: HTMLDetailsElement) {
const container = this.getContainer(detail);
this.toggleElement(container, true);
if (detail.dataset.wasOpen) {
detail.open = JSON.parse(detail.dataset.wasOpen);
delete detail.dataset.wasOpen;
}
}

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.allHeadings.forEach((heading) => this.hideHeading(heading));
this.allParents.forEach((parent) => this.collapseParent(parent));

// Show/hide items (+ show parents).
const terms = splitQuery(query);
let matchCount = 0;
this.items.forEach(({ haystack, link, container, heading, parents }) => {
this.resetHighlighting(link);
const isMatch = terms.every((needle) => haystack.includes(needle));

this.toggleElement(container, isMatch);

if (isMatch) {
matchCount++;
this.highlightMatches(link, terms);
if (heading) {
this.showHeading(heading);
}
for (const parent of parents) {
this.expandParent(parent);
}
}
});

return matchCount;
}

private hideHeading(heading: HTMLElement) {
const container = this.getContainer(heading);
this.toggleElement(container, false);
}

private collapseParent(el: HTMLDetailsElement) {
const container = this.getContainer(el);
this.toggleElement(container, false);
el.dataset.wasOpen = el.dataset.wasOpen ?? String(el.open);
el.open = false;
}

private highlightMatches(el: HTMLElement, terms: string[]) {
const nodes = this.getTextNodes(el);

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
);

const span = this.replaceChildNode(node, "span");
span.className = "highlight-container";

let rest = span.childNodes[0] as Node & Text;
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 node to HTML element.
this.replaceChildNode(match, "mark");

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

private getTextNodes(parentNode: Node): (Node & Text)[] {
const parents = [parentNode];
const nodes: (Node & Text)[] = [];

for (const parent of parents) {
for (const childNode of parent.childNodes) {
if (childNode.nodeType === Node.TEXT_NODE) {
nodes.push(childNode as Node & Text);
} else if (childNode.hasChildNodes()) {
parents.push(childNode);
}
}
}

return nodes;
}

private replaceChildNode(node: ChildNode, tagName: string) {
const text = node.textContent;
const newNode = document.createElement(tagName);
newNode.innerText = text ?? "";
node.replaceWith(newNode);
return newNode;
}

private showHeading(heading: HTMLElement) {
const container = heading && this.getContainer(heading);
if (container) {
this.toggleElement(container, true);
}
}

private getHeading(el: HTMLElement) {
return this.findFirstElementBefore(el, this.allHeadings);
}

private findFirstElementBefore(
target: HTMLElement,
candidates: HTMLElement[]
) {
return candidates
.slice()
.reverse()
.find(
(candidate) =>
candidate.compareDocumentPosition(target) &
Node.DOCUMENT_POSITION_FOLLOWING
);
}

private expandParent(parent: HTMLDetailsElement) {
const container = this.getContainer(parent);
this.toggleElement(container, true);
parent.open = true;
}

private getParents(el: HTMLElement) {
const parents: HTMLDetailsElement[] = [];
let parent = el.parentElement?.closest("details");

while (parent) {
if (parent instanceof HTMLDetailsElement) {
parents.push(parent);
}
parent = parent.parentElement?.closest("details");
}

return parents;
}
}
Loading