Skip to content

Commit c420ae5

Browse files
committed
Add more previews for FilterableTreeView
1 parent 619b617 commit c420ae5

5 files changed

Lines changed: 221 additions & 1 deletion

File tree

previews/primer/open_project/filterable_tree_view_preview.rb

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ module OpenProject
66
class FilterableTreeViewPreview < ViewComponent::Preview
77
# @label Playground
88
#
9-
# @snapshot interactive
109
# @param expanded [Boolean] toggle
1110
def playground(expanded: true)
1211
render_with_template(locals: {
@@ -33,6 +32,80 @@ def form_input(expanded: true)
3332
})
3433
end
3534

35+
# @label Custom segmented control
36+
#
37+
# @snapshot interactive
38+
#
39+
# ---------------------------
40+
# ## Custom filter modes
41+
#
42+
# In addition to the default filter modes of `'all'` and `'selected'`, `FilterableTreeView` supports
43+
# adding custom filter modes. Adding a filter mode will cause its label to appear in the `SegmentedControl` in the
44+
# toolbar, and will be passed as the third argument to the filter function (see below).
45+
#
46+
# Here's how to add a custom filter mode in addition to the default ones:
47+
#
48+
# ```erb
49+
# <%= render(Primer::OpenProject::FilterableTreeView.new) do |tree_view| %>
50+
# <%# remove this line to prevent adding the default modes %>
51+
# <% tree_view.with_default_filter_modes %>
52+
# <% tree_view.with_filter_mode(name: "Custom", system_arguments)
53+
# <% end %>
54+
# ```
55+
#
56+
# ### Customizing the filter function
57+
#
58+
# The filter function can be customized by setting the value of the `filterFn` property to a function with the
59+
# following signature:
60+
#
61+
# ```typescript
62+
# export type FilterFn = (node: HTMLElement, query: string, filterMode?: string) => Range[] | null
63+
# ```
64+
#
65+
# This function will be called once for each node in the tree every time filter controls change (i.e. when the
66+
# filter mode or query string are altered). The function is called with the following arguments:
67+
#
68+
# |Argument |Description |
69+
# |:-----------|:----------------------------------------------------------------|
70+
# |`node` |The HTML node element, i.e. the element with `role=treeitem` set.|
71+
# |`query` |The query string. |
72+
# |`filterMode`|The filter mode, either `'all'` or `'selected'`. |
73+
#
74+
# The component expects the filter function to return specific values depending on the type of match:
75+
#
76+
# 1. No match - return `null`
77+
# 2. Match but no highlights (eg. when the query string is empty) - return an empty array
78+
# 3. Match with highlights - return a non-empty array of `Range` objects
79+
#
80+
# Example:
81+
#
82+
# ```javascript
83+
# const filterableTreeView = document.querySelector('filterable-tree-view')
84+
# filterableTreeView.filterFn = (node, query, filterMode) => {
85+
# // custom filter implementation here
86+
# }
87+
# ```
88+
#
89+
def custom_segmented_control(expanded: true)
90+
render_with_template(locals: {
91+
expanded: coerce_bool(expanded)
92+
})
93+
end
94+
95+
# @label Custom no results text
96+
def custom_no_results_text(expanded: true)
97+
render_with_template(locals: {
98+
expanded: coerce_bool(expanded)
99+
})
100+
end
101+
102+
# @label Custom checkbox text
103+
def custom_checkbox_text(expanded: true)
104+
render_with_template(locals: {
105+
expanded: coerce_bool(expanded)
106+
})
107+
end
108+
36109
private
37110

38111
def coerce_bool(value)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script>
2+
function ready(fn) {
3+
if (document.readyState !== 'loading') {
4+
fn()
5+
} else {
6+
document.addEventListener('DOMContentLoaded', fn)
7+
}
8+
}
9+
10+
ready(() => {
11+
const filterableTreeView = document.querySelector('filterable-tree-view')
12+
filterableTreeView.filterFn = (node, query, filterMode) => {
13+
const ranges = []
14+
15+
if (query.length > 0) {
16+
const lowercaseQuery = query.toLowerCase()
17+
const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT)
18+
let currentNode = treeWalker.nextNode()
19+
20+
while (currentNode) {
21+
const lowercaseNodeText = currentNode.textContent?.toLocaleLowerCase() || ''
22+
let startIndex = 0
23+
24+
while (startIndex < lowercaseNodeText.length) {
25+
const index = lowercaseNodeText.indexOf(lowercaseQuery, startIndex)
26+
if (index === -1) break
27+
28+
const range = new Range()
29+
range.setStart(currentNode, index)
30+
range.setEnd(currentNode, index + lowercaseQuery.length)
31+
ranges.push(range)
32+
33+
startIndex = index + lowercaseQuery.length
34+
}
35+
36+
currentNode = treeWalker.nextNode()
37+
}
38+
}
39+
40+
if (ranges.length === 0 && query.length > 0) {
41+
return null
42+
}
43+
44+
switch (filterMode) {
45+
case 'teacher': {
46+
// Only match nodes that are teachers
47+
if (!node.dataset.path.includes('Students')) {
48+
return ranges
49+
}
50+
51+
break
52+
}
53+
54+
case 'all': {
55+
return ranges
56+
}
57+
}
58+
59+
return null
60+
}
61+
})
62+
</script>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<%= render(Primer::OpenProject::FilterableTreeView.new(include_sub_items_check_box_arguments: { name: :include_sub_items, label: "Including pupils" })) do |tree| %>
2+
<% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %>
3+
<% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %>
4+
<% ravenclaw.with_leaf(label: "Luna Lovegood") %>
5+
<% end %>
6+
7+
<% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %>
8+
<% hufflepuff.with_leaf(label: "Draco Malfoy") %>
9+
<% end %>
10+
11+
<% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %>
12+
<% hufflepuff.with_leaf(label: "Susan Bones") %>
13+
<% end %>
14+
15+
<% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %>
16+
<% hufflepuff.with_leaf(label: "Harry Potter") %>
17+
<% hufflepuff.with_leaf(label: "Ronald Weasley") %>
18+
<% hufflepuff.with_leaf(label: "Hermione Granger") %>
19+
<% end %>
20+
<% end %>
21+
22+
<% tree.with_leaf(label: "Albus Dumbledore") %>
23+
<% tree.with_leaf(label: "Minerva McGonagall") %>
24+
<% tree.with_leaf(label: "Severus Snape") %>
25+
<% tree.with_leaf(label: "Rubeus Hagrid") %>
26+
<% end %>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<%= render(Primer::OpenProject::FilterableTreeView.new(
2+
filter_input_arguments: {placeholder: "Search me!", name: "filter", label: "Filter", visually_hide_label: true},
3+
no_results_node_arguments: { label: "All wizards and witches have left the building" })) do |tree| %>
4+
<% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %>
5+
<% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %>
6+
<% ravenclaw.with_leaf(label: "Luna Lovegood") %>
7+
<% end %>
8+
9+
<% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %>
10+
<% hufflepuff.with_leaf(label: "Draco Malfoy") %>
11+
<% end %>
12+
13+
<% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %>
14+
<% hufflepuff.with_leaf(label: "Susan Bones") %>
15+
<% end %>
16+
17+
<% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %>
18+
<% hufflepuff.with_leaf(label: "Harry Potter") %>
19+
<% hufflepuff.with_leaf(label: "Ronald Weasley") %>
20+
<% hufflepuff.with_leaf(label: "Hermione Granger") %>
21+
<% end %>
22+
<% end %>
23+
24+
<% tree.with_leaf(label: "Albus Dumbledore") %>
25+
<% tree.with_leaf(label: "Minerva McGonagall") %>
26+
<% tree.with_leaf(label: "Severus Snape") %>
27+
<% tree.with_leaf(label: "Rubeus Hagrid") %>
28+
<% end %>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<%= render(Primer::OpenProject::FilterableTreeView.new) do |tree| %>
2+
<% tree.with_filter_mode(name: "teacher", label: "Teacher") %>
3+
<% tree.with_filter_mode(name: "all", label: "All", selected: true) %>
4+
5+
<% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %>
6+
<% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %>
7+
<% ravenclaw.with_leaf(label: "Luna Lovegood") %>
8+
<% end %>
9+
10+
<% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %>
11+
<% hufflepuff.with_leaf(label: "Draco Malfoy") %>
12+
<% end %>
13+
14+
<% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %>
15+
<% hufflepuff.with_leaf(label: "Susan Bones") %>
16+
<% end %>
17+
18+
<% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %>
19+
<% hufflepuff.with_leaf(label: "Harry Potter") %>
20+
<% hufflepuff.with_leaf(label: "Ronald Weasley") %>
21+
<% hufflepuff.with_leaf(label: "Hermione Granger") %>
22+
<% end %>
23+
<% end %>
24+
25+
<% tree.with_leaf(label: "Albus Dumbledore") %>
26+
<% tree.with_leaf(label: "Minerva McGonagall") %>
27+
<% tree.with_leaf(label: "Severus Snape") %>
28+
<% tree.with_leaf(label: "Rubeus Hagrid") %>
29+
<% end %>
30+
31+
<%= render partial: "primer/open_project/filterable_tree_view_preview/custom_select_js", locals: { } %>

0 commit comments

Comments
 (0)