Skip to content

Commit 5751514

Browse files
committed
Filtering functionality
1 parent 5b773a4 commit 5751514

File tree

15 files changed

+419
-259
lines changed

15 files changed

+419
-259
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,20 @@ The `CbxTree` interface also inherits methods from its parent, [HTMLElement](htt
253253

254254
Validation-related methods [`checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/checkValidity), [`reportValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/reportValidity), and [`setValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity) are transparently exposed from the underlying `ElementInternals` object which allows the `<cbx-tree>` element participate in form validation.
255255

256+
### `CbxTree.filter()`
257+
258+
This method can be used to “filter” the tree by hiding those items that don’t meet custom criteria. The method accepts a single argument, a preficate function. The predicate is passed an object argument with item’s `title` and `value` as properties, and the return value must be `true` if the item passes the filter and `false` otherwise. It should be noted that if an item passes the filter, its *descendants* remain visible even if they themselves don’t satisfy the filtering condition.
259+
260+
```javascript
261+
const readingList = document.querySelector('[name="reading-list[]"]');
262+
const filterInput = document.getElementById('filter');
263+
filterInput.addEventListener('input', () => {
264+
const query = filterInput.value.trim().toLocaleLowerCase();
265+
const predicate = query.length ? ({title}) => title.toLocaleLowerCase().includes(query) : () => true;
266+
readingList.filter(predicate);
267+
});
268+
```
269+
256270
### `CbxTree.setData()`
257271

258272
The `setData()` method is used for complete overwriting and rerendering the entire tree. It accepts a single argument, a new [tree data](#tree-data-structure). All existing changes will be lost and replaced by the newly provided data after calling this method. See an example in the [Usage notes](#usage-notes) section.

dist/cbx-tree.d.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default class CbxTree extends HTMLElement {
3434
toJSON(): CbxRawTreeItem[];
3535
toggleChecked(checked?: boolean): void;
3636
toggle(isExpanding?: boolean): void;
37+
filter(predicate: ({title: string, value: string}) => boolean): void;
3738

3839
get validity(): ValidityState;
3940
get validationMessage(): string;

dist/cbx-tree.mjs

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

docs/cbx-tree.mjs

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

docs/filtering.mjs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const tree = document.getElementById('file-tree');
2+
3+
const textNodes = [];
4+
export const collectTextNodes = () => {
5+
textNodes.length = 0;
6+
const treeWalker = document.createTreeWalker(tree.shadowRoot.firstElementChild, NodeFilter.SHOW_TEXT);
7+
let currentNode = treeWalker.nextNode();
8+
while (currentNode) {
9+
textNodes.push(currentNode);
10+
currentNode = treeWalker.nextNode();
11+
}
12+
};
13+
14+
const addHighlightCSS = () => {
15+
const stylesheet = new CSSStyleSheet();
16+
stylesheet.replaceSync('::highlight(filter-results){background-color:#ff0066;color:#fff;}');
17+
tree.shadowRoot.adoptedStyleSheets.push(stylesheet);
18+
};
19+
20+
// https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API#highlighting_search_results
21+
const highlightMatches = (query) => {
22+
if (!CSS.highlights) {
23+
return;
24+
}
25+
CSS.highlights.clear();
26+
if (!query) {
27+
return;
28+
}
29+
const ranges = textNodes.map((textNode) => {
30+
const text = textNode.textContent.toLowerCase();
31+
const indices = [];
32+
let startPos = 0;
33+
while (startPos < text.length) {
34+
const index = text.indexOf(query, startPos);
35+
if (index === -1) break;
36+
indices.push(index);
37+
startPos = index + query.length;
38+
}
39+
return indices.map((index) => {
40+
const range = new Range();
41+
range.setStart(textNode, index);
42+
range.setEnd(textNode, index + query.length);
43+
return range;
44+
});
45+
});
46+
CSS.highlights.set('filter-results', new Highlight(...ranges.flat()));
47+
};
48+
49+
customElements.whenDefined('cbx-tree').then(() => {
50+
addHighlightCSS();
51+
collectTextNodes();
52+
const {subtreeProvider} = tree;
53+
if (subtreeProvider) {
54+
tree.subtreeProvider = (...args) => subtreeProvider(...args).finally(() => setTimeout(collectTextNodes, 0));
55+
}
56+
});
57+
58+
let timer = null;
59+
document.getElementById('filter').addEventListener('input', ({target}) => {
60+
if (timer) {
61+
return;
62+
}
63+
timer = setTimeout(() => {
64+
timer = null;
65+
const query = target.value.trim().toLocaleLowerCase();
66+
const predicate = query.length ? ({title}) => title.toLocaleLowerCase().includes(query) : () => true;
67+
tree.filter(predicate);
68+
if (query.length) {
69+
tree.toggle(true);
70+
}
71+
highlightMatches(query);
72+
}, 250);
73+
});

docs/index.css

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,38 @@ button {
4646
}
4747

4848
.archive-head {
49-
align-items: baseline;
49+
align-items: center;
5050
display: flex;
5151
gap: 0.8ch;
5252

53+
h2 {
54+
flex-shrink: 0;
55+
}
56+
5357
button {
5458
align-items: center;
5559
display: flex;
5660
justify-content: center;
61+
}
62+
63+
[type='search'] {
64+
flex: 0 1 400px;
65+
height: 1.8lh;
66+
margin-inline-start: auto;
67+
}
68+
69+
@media (width < 600px) {
70+
flex-wrap: wrap;
5771

58-
&:first-of-type {
72+
button:first-of-type {
5973
margin-inline-start: auto;
6074
}
75+
76+
[type='search'] {
77+
flex-grow: 1;
78+
margin: 0;
79+
order: 1;
80+
}
6181
}
6282
}
6383

docs/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
</head>
1818
<body>
1919
<h1><code>&lt;cbx-tree&gt;</code>: The Checkbox Tree element</h1>
20-
<form action="." id="archive-form" class="archive-form">
20+
<form action="." id="archive-form" class="archive-form" autocomplete="off">
2121
<div class="archive-head">
2222
<h2>Select files/directories</h2>
23+
<input type="search" id="filter" placeholder="Filter the tree">
2324
<button type="button" id="toggle-all-btn" title="Expand/collapse all">
2425
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1lh" height="1lh" fill="currentColor">
2526
<path d="M3.65 9.15a.5.5 0 0 1 .7 0L8 12.79l3.65-3.64a.5.5 0 0 1 .7.7l-4 4a.5.5 0 0 1-.7 0l-4-4a.5.5 0 0 1 0-.7m0-2.3a.5.5 0 0 0 .7 0L8 3.21l3.65 3.64a.5.5 0 0 0 .7-.7l-4-4a.5.5 0 0 0-.7 0l-4 4a.5.5 0 0 0 0 .7"/>
@@ -46,5 +47,6 @@ <h2>Select files/directories</h2>
4647
</footer>
4748
<script type="module" src="./cbx-tree.mjs"></script>
4849
<script type="module" src="./index.mjs"></script>
50+
<script type="module" src="./filtering.mjs"></script>
4951
</body>
5052
</html>

index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</head>
99
<body>
1010
<form action="#">
11+
<input id="filter" class="filter" type="search">
1112
<cbx-tree name="reading-list[]">
1213
<script type="application/json">
1314
[
@@ -34,12 +35,12 @@
3435
"checked": true
3536
},
3637
{
37-
"title": "Theogony, <i>by Hesoid</i>",
38+
"title": "Theogony, <i>by Hesiod</i>",
3839
"value": "theogony",
3940
"icon": "./icons/manuscript.svg"
4041
},
4142
{
42-
"title": "Works and Days, <i>by Hesoid</i>",
43+
"title": "Works and Days, <i>by Hesiod</i>",
4344
"value": "works-and-days",
4445
"icon": "./icons/manuscript.svg"
4546
},

0 commit comments

Comments
 (0)