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

Add search to sidebar #215

Merged
merged 49 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
88ff7c1
remove package json
robonetphy Jul 17, 2022
97a019b
twig file modified
robonetphy Jul 20, 2022
44e9b50
search bar style added
robonetphy Jul 20, 2022
c532e20
the background content added
robonetphy Jul 21, 2022
0d35845
add the switching b/w the shortcut logo
robonetphy Jul 22, 2022
c05e1f6
shortcut for search added
robonetphy Jul 22, 2022
7bea909
add the arrowup and arrowdown short cut
robonetphy Jul 22, 2022
80d2b9d
sidebar search added
robonetphy Jul 23, 2022
dbce38a
keyup and keydown replace with input
robonetphy Jul 23, 2022
22f4a59
the sidebar search selected added
robonetphy Jul 23, 2022
7e33388
unusal things
robonetphy Jul 23, 2022
8475e22
the enter evenlister added with search refactring
robonetphy Jul 23, 2022
8cd1b12
comments added
robonetphy Jul 23, 2022
ad5f9cb
the scroll added if element is not visble
robonetphy Jul 23, 2022
f0225b8
resolve the conflict
robonetphy Jul 23, 2022
46669dc
metakey added
robonetphy Jul 25, 2022
30c4971
Merge branch 'main' into feat/sidebar-search
robonetphy Aug 3, 2022
ba9e854
Merge branch 'main' into feat/sidebar-search
robonetphy Aug 28, 2022
8b90e95
event listner using shortcut added
robonetphy Aug 28, 2022
0ae8810
the integration for input box completed
robonetphy Aug 28, 2022
09706f0
nodemon config updated
robonetphy Aug 28, 2022
8277fc7
replace the shortcuts with event listener
robonetphy Aug 30, 2022
6c4769c
bugfix: up height of header added
robonetphy Aug 30, 2022
ab96ea0
feat:integrate sidebar toggle with search shortcut
robonetphy Aug 30, 2022
a463ef2
syntax improved
robonetphy Aug 30, 2022
82af94b
event listener updated
robonetphy Sep 1, 2022
cb1ce28
border adjusted
robonetphy Sep 5, 2022
480f79d
search adjusted
robonetphy Sep 5, 2022
97555e0
sidebar search navigation adjusted
robonetphy Sep 5, 2022
9a0d083
Merge branch 'main' into feat/sidebar-search
neSpecc Sep 14, 2022
2e9ea2e
new search module added
robonetphy Sep 19, 2022
80f3a59
new module integrated
robonetphy Sep 19, 2022
5cc520b
boxshadow added as border
robonetphy Sep 19, 2022
8a86538
sidebar search class added
robonetphy Sep 19, 2022
0619af1
sidebar search=>filter
robonetphy Sep 19, 2022
8fd0b7a
comments added
robonetphy Sep 19, 2022
1af4e05
filter for section added
robonetphy Sep 19, 2022
de7b346
the expand feature added during navigation
robonetphy Sep 19, 2022
1156031
Merge branch 'feat/sidebar-search' of https://github.com/codex-team/c…
robonetphy Sep 19, 2022
0c48dea
remove the space
robonetphy Sep 19, 2022
49d610f
header height variable added
robonetphy Oct 11, 2022
5ff587a
shortcut logic updated
robonetphy Oct 12, 2022
294fce5
enum for direction added
robonetphy Oct 12, 2022
5b668f7
common search function added
robonetphy Oct 12, 2022
6be524d
expand every match
robonetphy Oct 12, 2022
04f836f
updated styles
robonetphy Oct 12, 2022
c82f06a
updated styles
robonetphy Oct 12, 2022
acf368d
margin remove in mobile view with bold removed
robonetphy Oct 12, 2022
ea72505
clean css added
robonetphy Oct 12, 2022
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
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"watch": [
"**/*"
],
"ext": "js,twig"
"ext": "ts,js,twig"
}
1 change: 1 addition & 0 deletions src/backend/controllers/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ class Pages {
await alias.destroy();
}
const removedPage = page.destroy();

await PagesFlatArray.regenerate();

return removedPage;
Expand Down
3 changes: 3 additions & 0 deletions src/backend/views/components/sidebar.twig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
</div>

<aside class="docs-sidebar__content docs-sidebar__content--invisible">
<span class="docs-sidebar__search-wrapper">
<input class="docs-sidebar__search" type="text" placeholder="Search" />
</span>
{% for firstLevelPage in menu %}
<section class="docs-sidebar__section" data-id="{{firstLevelPage._id}}">
<a class="docs-sidebar__section-title-wrapper"
Expand Down
365 changes: 365 additions & 0 deletions src/frontend/js/classes/sidebar-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
/**
* HEIGHT of the header in px
*/
const HEADER_HEIGHT = parseInt(window.getComputedStyle(
document.documentElement).getPropertyValue('--layout-height-header'));

/**
* Enum for the direction of the navigation during the filtering.
*/
const Direction = {
Next: 1,
Previous: 2,
};

/**
* Sidebar Search module.
*/
export default class SidebarFilter {
/**
* CSS classes
*
* @returns {Record<string, string>}
*/
static get CSS() {
return {
sectionHidden: 'docs-sidebar__section--hidden',
sectionTitle: 'docs-sidebar__section-title',
sectionTitleSelected: 'docs-sidebar__section-title--selected',
sectionTitleActive: 'docs-sidebar__section-title--active',
sectionList: 'docs-sidebar__section-list',
sectionListItem: 'docs-sidebar__section-list-item',
sectionListItemWrapperHidden: 'docs-sidebar__section-list-item-wrapper--hidden',
sectionListItemSlelected: 'docs-sidebar__section-list-item--selected',
sidebarSearchWrapper: 'docs-sidebar__search-wrapper',
};
}

/**
* Creates base properties
*/
constructor() {
/**
* Stores refs to HTML elements needed for sidebar filter to work.
*/
this.sidebar = null;
this.sections = [];
this.sidebarContent = null;
this.search = null;
this.searchResults = [];
this.selectedSearchResultIndex = null;
}

/**
* Initialize sidebar filter.
*
* @param {HTMLElement[]} sections - Array of sections.
* @param {HTMLElement} sidebarContent - Sidebar content.
* @param {HTMLElement} search - Search input.
* @param {Function} setSectionCollapsed - Function to set section collapsed.
*/
init(sections, sidebarContent, search, setSectionCollapsed) {
// Store refs to HTML elements.
this.sections = sections;
this.sidebarContent = sidebarContent;
this.search = search;
this.setSectionCollapsed = setSectionCollapsed;
let shortcutText = 'Ctrl P';

// Search initialize with platform specific shortcut.
if (window.navigator.userAgent.indexOf('Mac') !== -1) {
shortcutText = '⌘ P';
}
shortcutText = '⌘ P';
robonetphy marked this conversation as resolved.
Show resolved Hide resolved
this.search.parentElement.setAttribute('data-shortcut', shortcutText);

// Initialize search input.
this.search.value = '';

// Add event listener for search input.
this.search.addEventListener('input', e => {
e.stopImmediatePropagation();
e.preventDefault();
this.filter(e.target.value);
});
// Add event listener for keyboard events.
this.search.addEventListener('keydown', e => this.handleKeyboardEvent(e));
}

/**
* Handle keyboard events while search input is focused.
*
* @param {Event} e - Event Object.
*/
handleKeyboardEvent(e) {
// Return if search is not focused.
if (this.search !== document.activeElement) {
return;
}

// handle enter key when item is focused.
if (e.code === 'Enter' && this.selectedSearchResultIndex !== null) {
// navigate to focused item.
this.searchResults[this.selectedSearchResultIndex].element.click();
// prevent default action.
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}

// handle up and down navigation.
if (e.code === 'ArrowUp' || e.code === 'ArrowDown') {
// check for search results.
if (this.searchResults.length === 0) {
return;
}

// get current focused item.
const prevSelectedSearchResultIndex = this.selectedSearchResultIndex;

// get next item to be focus.
if (e.code === 'ArrowUp') {
this.selectedSearchResultIndex = this.getNextIndex(
Direction.Previous,
this.selectedSearchResultIndex,
this.searchResults.length - 1);
} else if (e.code === 'ArrowDown') {
this.selectedSearchResultIndex = this.getNextIndex(
Direction.Next,
this.selectedSearchResultIndex,
this.searchResults.length - 1);
}

// blur previous focused item.
this.blurTitleOrItem(prevSelectedSearchResultIndex);
// focus next item.
this.focusTitleOrItem(this.selectedSearchResultIndex);

// prevent default action.
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
}

/**
* Get index of next item to be focused.
*
* @param {number} direction - direction for navigation.
* @param {number} titleOrItemIndex - Current title or item index.
* @param {number} maxNumberOfTitlesOrItems - Max number of titles or items.
* @returns {number} - Next section or item index.
*/
getNextIndex(direction, titleOrItemIndex, maxNumberOfTitlesOrItems) {
let nextTitleOrItemIndex = titleOrItemIndex;

if (direction === Direction.Previous) {
// if no item is focused, focus last item.
if (titleOrItemIndex === null) {
return maxNumberOfTitlesOrItems;
}

// focus previous item.
nextTitleOrItemIndex--;

// circular navigation.
if (nextTitleOrItemIndex < 0) {
nextTitleOrItemIndex = maxNumberOfTitlesOrItems;
}

return nextTitleOrItemIndex;
} else if (direction === Direction.Next) {
// if no item is focused, focus first item.
if (titleOrItemIndex === null) {
return 0;
}

// focus next item.
nextTitleOrItemIndex++;

// circular navigation.
if (nextTitleOrItemIndex > maxNumberOfTitlesOrItems) {
nextTitleOrItemIndex = 0;
}

return nextTitleOrItemIndex;
}
}

/**
* Focus title or item at given index.
*
* @param {number} titleOrItemIndex - Title or item index.
*/
focusTitleOrItem(titleOrItemIndex) {
// check for valid index.
if (titleOrItemIndex === null) {
return;
}

const { element, type } = this.searchResults[titleOrItemIndex];

if (!element || !type) {
return;
}

// focus title or item.
if (type === 'title') {
element.classList.add(SidebarFilter.CSS.sectionTitleSelected);
} else if (type === 'item') {
element.classList.add(SidebarFilter.CSS.sectionListItemSlelected);
}

// scroll to focused title or item.
this.scrollToTitleOrItem(element);
}

/**
* Blur title or item at given index.
*
* @param {number} titleOrItemIndex - Title or item index.
*/
blurTitleOrItem(titleOrItemIndex) {
// check for valid index.
if (titleOrItemIndex === null) {
return;
}

const { element, type } = this.searchResults[titleOrItemIndex];

if (!element || !type) {
return;
}

// blur title or item.
if (type === 'title') {
element.classList.remove(SidebarFilter.CSS.sectionTitleSelected);
} else if (type === 'item') {
element.classList.remove(SidebarFilter.CSS.sectionListItemSlelected);
}
}

/**
* Scroll to title or item.
*
* @param {HTMLElement} titleOrItem - Title or item element.
*/
scrollToTitleOrItem(titleOrItem) {
// check if it's visible.
const rect = titleOrItem.getBoundingClientRect();
let elemTop = rect.top;
let elemBottom = rect.bottom;
const halfOfViewport = window.innerHeight / 2;
const scrollTop = this.sidebarContent.scrollTop;

// scroll top if item is not visible.
if (elemTop < HEADER_HEIGHT) {
// scroll half viewport up.
const nextTop = scrollTop - halfOfViewport;

// check if element visible after scroll.
elemTop = (elemTop + nextTop) < HEADER_HEIGHT ? elemTop : nextTop;
this.sidebarContent.scroll({
top: elemTop,
behavior: 'smooth',
});
} else if (elemBottom > window.innerHeight) {
// scroll bottom if item is not visible.
// scroll half viewport down.
const nextDown = halfOfViewport + scrollTop;

// check if element visible after scroll.
elemBottom = (elemBottom - nextDown) > window.innerHeight ? elemBottom : nextDown;
this.sidebarContent.scroll({
top: elemBottom,
behavior: 'smooth',
});
}
}

/**
* Check if content contains search text.
*
* @param {string} content - content to be searched.
* @param {string} searchValue - Search value.
* @returns {boolean} - true if content contains search value.
*/
isValueMatched(content, searchValue) {
return content.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1;
}

/**
* filter sidebar items.
*
* @param {HTMLElement} section - Section element.
* @param {string} searchValue - Search value.
*/
filterSection(section, searchValue) {
// match with section title.
const sectionTitle = section.querySelector('.' + SidebarFilter.CSS.sectionTitle);
const sectionList = section.querySelector('.' + SidebarFilter.CSS.sectionList);

// check if section title matches.
const isTitleMatch = this.isValueMatched(sectionTitle.textContent, searchValue);

const matchResults = [];
// match with section items.
let isSingleItemMatch = false;

if (sectionList) {
const sectionListItems = sectionList.querySelectorAll('.' + SidebarFilter.CSS.sectionListItem);

sectionListItems.forEach(item => {
if (this.isValueMatched(item.textContent, searchValue)) {
// remove hiden class from item.
item.parentElement.classList.remove(SidebarFilter.CSS.sectionListItemWrapperHidden);
// add item to search results.
matchResults.push({
element: item,
type: 'item',
});
isSingleItemMatch = true;
} else {
// hide item if it is not a match.
item.parentElement.classList.add(SidebarFilter.CSS.sectionListItemWrapperHidden);
}
});
}
if (!isTitleMatch && !isSingleItemMatch) {
// hide section if it's items are not a match.
section.classList.add(SidebarFilter.CSS.sectionHidden);
} else {
const parentSection = sectionTitle.closest('section');

// if item is in collapsed section, expand it.
if (!parentSection.classList.contains(SidebarFilter.CSS.sectionTitleActive)) {
this.setSectionCollapsed(parentSection, false);
}
// show section if it's items are a match.
section.classList.remove(SidebarFilter.CSS.sectionHidden);
// add section title to search results.
this.searchResults.push({
element: sectionTitle,
type: 'title',
}, ...matchResults);
}
}

/**
* Filter sidebar sections.
*
* @param {string} searchValue - Search value.
*/
filter(searchValue) {
// remove selection from previous search results.
this.blurTitleOrItem(this.selectedSearchResultIndex);
// empty selected index.
this.selectedSearchResultIndex = null;
// empty search results.
this.searchResults = [];
// match search value with sidebar sections.
this.sections.forEach(section => {
this.filterSection(section, searchValue);
});
}
}
Loading