Skip to content

Commit 56d8a79

Browse files
committed
Refactor sidebar component structure
1 parent 66ffd4f commit 56d8a79

6 files changed

Lines changed: 191 additions & 154 deletions

File tree

src/backend/views/components/sidebar-branch.twig

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% set is_leaf = node.children is not defined or node.children is empty %}
2+
<section class="docs-sidebar__section{{ nested ? ' docs-sidebar__section--nested' : '' }}{{ is_leaf ? ' docs-sidebar__section--leaf' : '' }}" data-id="{{ node._id }}">
3+
<a class="docs-sidebar__section-title-wrapper"
4+
href="{{ node.uri ? '/' ~ node.uri : '/page/' ~ node._id }}"
5+
>
6+
<div class="docs-sidebar__section-title {{ page is defined and toString(page._id) == toString(node._id) ? 'docs-sidebar__section-title--active' : '' }}">
7+
<span>
8+
{{ node.title | striptags }}
9+
</span>
10+
{% if node.children is defined and node.children is not empty %}
11+
<button type="button" class="docs-sidebar__section-toggler" aria-label="Toggle section">
12+
{{ svg('arrow-up') }}
13+
</button>
14+
{% endif %}
15+
</div>
16+
</a>
17+
{% if node.children is defined and node.children is not empty %}
18+
<ul class="docs-sidebar__section-list docs-sidebar__section-list--nested">
19+
{% for child in node.children %}
20+
<li>
21+
{% include 'components/sidebar-section.twig' with { node: child, nested: true } %}
22+
</li>
23+
{% endfor %}
24+
</ul>
25+
{% endif %}
26+
</section>

src/backend/views/components/sidebar.twig

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,7 @@
1010
<input class="docs-sidebar__search" type="text" placeholder="Search" />
1111
</span>
1212
{% for firstLevelPage in menu %}
13-
<section class="docs-sidebar__section" data-id="{{firstLevelPage._id}}">
14-
<a class="docs-sidebar__section-title-wrapper"
15-
href="{{firstLevelPage.uri ? '/' ~ firstLevelPage.uri : '/page/' ~ firstLevelPage._id }}"
16-
>
17-
<div class="docs-sidebar__section-title {{page is defined and page._id == firstLevelPage._id ? 'docs-sidebar__section-title--active' : ''}}">
18-
<span>
19-
{{ firstLevelPage.title | striptags }}
20-
</span>
21-
{% if firstLevelPage.children is not empty %}
22-
<button class="docs-sidebar__section-toggler">
23-
{{ svg('arrow-up') }}
24-
</button>
25-
{% endif %}
26-
</div>
27-
</a>
28-
{% if firstLevelPage.children is not empty %}
29-
{% include 'components/sidebar-branch.twig' with { nodes: firstLevelPage.children, ulClass: '' } %}
30-
{% endif %}
31-
</section>
13+
{% include 'components/sidebar-section.twig' with { node: firstLevelPage, nested: false } %}
3214
{% endfor %}
3315

3416
<div class="docs-sidebar__logo">

src/frontend/js/classes/sidebar-filter.js

Lines changed: 65 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@ export default class SidebarFilter {
2323
*/
2424
static get CSS() {
2525
return {
26+
section: 'docs-sidebar__section',
2627
sectionHidden: 'docs-sidebar__section--hidden',
2728
sectionTitle: 'docs-sidebar__section-title',
2829
sectionTitleSelected: 'docs-sidebar__section-title--selected',
2930
sectionTitleActive: 'docs-sidebar__section-title--active',
3031
sectionList: 'docs-sidebar__section-list',
31-
sectionListItem: 'docs-sidebar__section-list-item',
32-
sectionListItemWrapperHidden: 'docs-sidebar__section-list-item-wrapper--hidden',
33-
sectionListItemSlelected: 'docs-sidebar__section-list-item--selected',
3432
sidebarSearchWrapper: 'docs-sidebar__search-wrapper',
3533
};
3634
}
@@ -43,7 +41,7 @@ export default class SidebarFilter {
4341
* Stores refs to HTML elements needed for sidebar filter to work.
4442
*/
4543
this.sidebar = null;
46-
this.sections = [];
44+
this.rootSections = [];
4745
this.sidebarContent = null;
4846
this.search = null;
4947
this.searchResults = [];
@@ -53,14 +51,14 @@ export default class SidebarFilter {
5351
/**
5452
* Initialize sidebar filter.
5553
*
56-
* @param {HTMLElement[]} sections - Array of sections.
54+
* @param {HTMLElement[]} rootSections - Top-level sections (direct children of sidebar content).
5755
* @param {HTMLElement} sidebarContent - Sidebar content.
5856
* @param {HTMLElement} search - Search input.
5957
* @param {Function} setSectionCollapsed - Function to set section collapsed.
6058
*/
61-
init(sections, sidebarContent, search, setSectionCollapsed) {
59+
init(rootSections, sidebarContent, search, setSectionCollapsed) {
6260
// Store refs to HTML elements.
63-
this.sections = sections;
61+
this.rootSections = rootSections;
6462
this.sidebarContent = sidebarContent;
6563
this.search = search;
6664
this.setSectionCollapsed = setSectionCollapsed;
@@ -202,11 +200,8 @@ export default class SidebarFilter {
202200
return;
203201
}
204202

205-
// focus title or item.
206203
if (type === 'title') {
207204
element.classList.add(SidebarFilter.CSS.sectionTitleSelected);
208-
} else if (type === 'item') {
209-
element.classList.add(SidebarFilter.CSS.sectionListItemSlelected);
210205
}
211206

212207
// scroll to focused title or item.
@@ -230,11 +225,8 @@ export default class SidebarFilter {
230225
return;
231226
}
232227

233-
// blur title or item.
234228
if (type === 'title') {
235229
element.classList.remove(SidebarFilter.CSS.sectionTitleSelected);
236-
} else if (type === 'item') {
237-
element.classList.remove(SidebarFilter.CSS.sectionListItemSlelected);
238230
}
239231
}
240232

@@ -294,54 +286,76 @@ export default class SidebarFilter {
294286
* @param {string} searchValue - Search value.
295287
*/
296288
filterSection(section, searchValue) {
297-
// match with section title.
298289
const sectionTitle = section.querySelector('.' + SidebarFilter.CSS.sectionTitle);
299-
const sectionList = section.querySelector('.' + SidebarFilter.CSS.sectionList);
290+
const sectionList = section.querySelector(':scope > .' + SidebarFilter.CSS.sectionList);
291+
292+
if (!sectionTitle) {
293+
return false;
294+
}
295+
296+
const empty = !searchValue || !searchValue.trim();
297+
298+
if (empty) {
299+
section.classList.remove(SidebarFilter.CSS.sectionHidden);
300+
301+
if (sectionList) {
302+
Array.from(sectionList.children).forEach((li) => {
303+
const nestedSection = li.querySelector(':scope > .' + SidebarFilter.CSS.section);
304+
305+
if (nestedSection) {
306+
this.filterSection(nestedSection, searchValue);
307+
}
308+
});
309+
}
310+
311+
return true;
312+
}
300313

301-
// check if section title matches.
302314
const isTitleMatch = this.isValueMatched(sectionTitle.textContent, searchValue);
315+
let hasMatch = isTitleMatch;
303316

304-
const matchResults = [];
305-
// match with section items.
306-
let isSingleItemMatch = false;
317+
if (isTitleMatch) {
318+
this.searchResults.push({
319+
element: sectionTitle,
320+
type: 'title',
321+
});
322+
}
307323

308324
if (sectionList) {
309-
const sectionListItems = sectionList.querySelectorAll('.' + SidebarFilter.CSS.sectionListItem);
310-
311-
sectionListItems.forEach(item => {
312-
if (this.isValueMatched(item.textContent, searchValue)) {
313-
// remove hiden class from item.
314-
item.parentElement.classList.remove(SidebarFilter.CSS.sectionListItemWrapperHidden);
315-
// add item to search results.
316-
matchResults.push({
317-
element: item,
318-
type: 'item',
319-
});
320-
isSingleItemMatch = true;
321-
} else {
322-
// hide item if it is not a match.
323-
item.parentElement.classList.add(SidebarFilter.CSS.sectionListItemWrapperHidden);
325+
for (const li of sectionList.children) {
326+
const nestedSection = li.querySelector(':scope > .' + SidebarFilter.CSS.section);
327+
328+
if (!nestedSection) {
329+
continue;
324330
}
325-
});
326-
}
327-
if (!isTitleMatch && !isSingleItemMatch) {
328-
// hide section if it's items are not a match.
329-
section.classList.add(SidebarFilter.CSS.sectionHidden);
330-
} else {
331-
const parentSection = sectionTitle.closest('section');
332331

333-
// if item is in collapsed section, expand it.
334-
if (!parentSection.classList.contains(SidebarFilter.CSS.sectionTitleActive)) {
335-
this.setSectionCollapsed(parentSection, false);
332+
const childHasMatch = this.filterSection(nestedSection, searchValue);
333+
334+
hasMatch = hasMatch || childHasMatch;
336335
}
337-
// show section if it's items are a match.
336+
}
337+
338+
if (hasMatch) {
338339
section.classList.remove(SidebarFilter.CSS.sectionHidden);
339-
// add section title to search results.
340-
this.searchResults.push({
341-
element: sectionTitle,
342-
type: 'title',
343-
}, ...matchResults);
340+
this.setSectionCollapsed(section, false);
341+
342+
let el = section.parentElement;
343+
344+
while (el && el !== this.sidebarContent) {
345+
const ancSection = el.closest('.' + SidebarFilter.CSS.section);
346+
347+
if (!ancSection) {
348+
break;
349+
}
350+
351+
this.setSectionCollapsed(ancSection, false);
352+
el = ancSection.parentElement;
353+
}
354+
} else {
355+
section.classList.add(SidebarFilter.CSS.sectionHidden);
344356
}
357+
358+
return hasMatch;
345359
}
346360

347361
/**
@@ -356,8 +370,7 @@ export default class SidebarFilter {
356370
this.selectedSearchResultIndex = null;
357371
// empty search results.
358372
this.searchResults = [];
359-
// match search value with sidebar sections.
360-
this.sections.forEach(section => {
373+
this.rootSections.forEach(section => {
361374
this.filterSection(section, searchValue);
362375
});
363376
}

0 commit comments

Comments
 (0)