1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-07 14:35:26 +02:00

sidebar search=>filter

This commit is contained in:
Umang G. Patel 2022-09-19 23:35:19 +05:30
parent 8a865383fa
commit 0619af1bd0
3 changed files with 275 additions and 266 deletions

View file

@ -0,0 +1,270 @@
/**
* HEIGHT of the header in px
*/
const HEADER_HEIGHT = 56;
/**
* 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',
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',
sidebarSearchWrapperMac: 'docs-sidebar__search-wrapper-mac',
sidebarSearchWrapperOther: 'docs-sidebar__search-wrapper-other',
};
}
/**
* 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;
}
init(sections, sidebarContent, search)
{
this.sections = sections;
this.sidebarContent = sidebarContent;
this.search = search;
let className = SidebarFilter.CSS.sidebarSearchWrapperOther;
// Search initialize with platform specific shortcut.
if (window.navigator.userAgent.indexOf('Mac') != -1) {
className = SidebarFilter.CSS.sidebarSearchWrapperMac;
}
this.search.parentElement.classList.add(className);
// Add event listener for search input.
this.search.addEventListener('input', e => {
e.stopImmediatePropagation();
e.preventDefault();
this.filterSections(e.target.value);
});
// Initialize the search results.
this.filterSections('');
// Add event listener for keyboard events.
this.search.addEventListener('keydown', e => this.handleKeyboardEvent(e));
}
/**
* Handle keyboard events when search input is focused.
*
* @param {Event} e - Event Object.
* @returns {void}
*/
handleKeyboardEvent(e) {
// Return if search is not focused.
if (this.search !== document.activeElement) {
return;
}
// if enter is pressed and item is focused, then click on focused item.
if (e.code === 'Enter' && this.selectedSearchResultIndex !== null) {
// goto focused item.
this.searchResults[this.selectedSearchResultIndex].element.click();
// prevent default action.
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
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;
this.selectedSearchResultIndex = this.getNextSectionOrItemIndex(e.code,
this.selectedSearchResultIndex,
this.searchResults.length - 1);
this.blurSectionOrItem(prevSelectedSearchResultIndex);
this.focusSectionOrItem(this.selectedSearchResultIndex);
// prevent default action.
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
}
getNextSectionOrItemIndex(code, sectionOrItemIndex, maxNumberOfSectionsOrItems) {
let nextSectionOrItemIndex = sectionOrItemIndex;
if (code === 'ArrowUp') {
if (sectionOrItemIndex === null) {
return maxNumberOfSectionsOrItems;
}
nextSectionOrItemIndex--;
if (nextSectionOrItemIndex < 0) {
nextSectionOrItemIndex = maxNumberOfSectionsOrItems;
}
return nextSectionOrItemIndex;
}
else if (code === 'ArrowDown') {
if (sectionOrItemIndex === null) {
return 0;
}
nextSectionOrItemIndex++;
if (nextSectionOrItemIndex > maxNumberOfSectionsOrItems) {
nextSectionOrItemIndex = 0;
}
return nextSectionOrItemIndex;
}
}
focusSectionOrItem(sectionOrItemIndex) {
if (sectionOrItemIndex === null) {
return;
}
const { element, type } = this.searchResults[sectionOrItemIndex];
if (!element || !type) {
return;
}
if (type === 'title') {
element.classList.add(SidebarFilter.CSS.sectionTitleSelected);
} else if (type === 'item') {
element.classList.add(SidebarFilter.CSS.sectionListItemSlelected);
}
this.scrollToSectionOrItem(element);
}
blurSectionOrItem(sectionOrItemIndex) {
if (sectionOrItemIndex === null) {
return;
}
const { element, type } = this.searchResults[sectionOrItemIndex];
if (!element || !type) {
return;
}
if (type === 'title') {
element.classList.remove(SidebarFilter.CSS.sectionTitleSelected);
} else if (type === 'item') {
element.classList.remove(SidebarFilter.CSS.sectionListItemSlelected);
}
}
scrollToSectionOrItem(sectionOrItem) {
// check if it's visible.
const rect = sectionOrItem.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',
});
}
}
filterSections(searchValue) {
// remove selection from previous search results.
this.blurSectionOrItem(this.selectedSearchResultIndex);
// empty selected index.
this.selectedSearchResultIndex = null;
// empty search results.
this.searchResults = [];
// match search value with sidebar sections.
this.sections.forEach(section => {
// match with section title.
const sectionTitle = section.querySelector('.' + SidebarFilter.CSS.sectionTitle);
const isTitleMatch = sectionTitle.innerText.trim().toLowerCase()
.indexOf(searchValue.toLowerCase()) !== -1;
const matchResults = [];
// match with section items.
const sectionList = section.querySelector('.' + SidebarFilter.CSS.sectionList);
let isSingleItemMatch = false;
if (sectionList) {
const sectionListItems = sectionList.querySelectorAll('.' + SidebarFilter.CSS.sectionListItem);
sectionListItems.forEach(item => {
if (item.innerText.trim().toLowerCase()
.indexOf(searchValue.toLowerCase()) !== -1) {
// 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 and it's items are not a match.
section.classList.add(SidebarFilter.CSS.sectionHidden);
} else {
// show section and 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);
}
});
}
}

View file

@ -1,262 +0,0 @@
/**
* HEIGHT of the header in px
*/
const HEADER_HEIGHT = 56;
/**
* Sidebar module
*/
export default class SidebarSearch {
/**
* 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',
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',
sidebarSearchWrapperMac: 'docs-sidebar__search-wrapper-mac',
sidebarSearchWrapperOther: 'docs-sidebar__search-wrapper-other',
};
}
/**
* Creates base properties
*/
constructor() {
/**
* Stores refs to HTML elements needed for correct sidebar work
*/
this.sidebar = null;
this.sections = [];
this.sidebarContent = null;
this.search = null;
this.searchResults = [];
this.selectedSearchResultIndex = null;
}
/**
*
* @param {*} sections
* @param {*} sidebarContent
* @param {*} search
*/
init(sections, sidebarContent, search)
{
this.sections = sections;
this.sidebarContent = sidebarContent;
this.search = search;
let className = SidebarSearch.CSS.sidebarSearchWrapperOther;
// Search initialize with platform specific shortcut.
if (window.navigator.userAgent.indexOf('Mac') != -1) {
className = SidebarSearch.CSS.sidebarSearchWrapperMac;
}
this.search.parentElement.classList.add(className);
// Add event listener for search input.
this.search.addEventListener('input', e => {
e.stopImmediatePropagation();
e.preventDefault();
this.filterSections(e.target.value);
});
// Initialize the search results.
this.filterSections('');
// Add event listener for keyboard events.
this.search.addEventListener('keydown', e => this.handleKeyboardEventOnSearch(e));
}
/**
* Handle keyboard events when search input is focused.
*
* @param {Event} e - Event Object.
* @returns {void}
*/
handleKeyboardEventOnSearch(e) {
// Return if search is not focused.
if (this.search !== document.activeElement) {
return;
}
// if enter is pressed and item is focused, then click on focused item.
if (e.code === 'Enter' && this.selectedSearchResultIndex !== null) {
// goto focused item.
this.searchResults[this.selectedSearchResultIndex].element.click();
// prevent default action.
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
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 index.
if (this.selectedSearchResultIndex === null) {
// if no item is focused and up press, focus last item.
if (e.code === 'ArrowUp') {
this.selectedSearchResultIndex = this.searchResults.length - 1;
} else if (e.code === 'ArrowDown') {
// if no item is focused and down press, focus first item.
this.selectedSearchResultIndex = 0;
}
} else {
// if item is focused and up press, focus previous item.
if (e.code === 'ArrowUp') {
this.selectedSearchResultIndex--;
if (this.selectedSearchResultIndex < 0) {
this.selectedSearchResultIndex = this.searchResults.length - 1;
}
} else if (e.code === 'ArrowDown') {
// if item is focused and down press, focus next item.
this.selectedSearchResultIndex++;
if (this.selectedSearchResultIndex >= this.searchResults.length) {
this.selectedSearchResultIndex = 0;
}
}
}
// remove focus from previous item.
if (prevSelectedSearchResultIndex !== null) {
const { element: preElement, type: preType } = this.searchResults[prevSelectedSearchResultIndex];
if (preElement) {
// remove focus from previous item or title.
if (preType === 'title') {
preElement.classList.remove(SidebarSearch.CSS.sectionTitleSelected);
} else if (preType === 'item') {
preElement.classList.remove(SidebarSearch.CSS.sectionListItemSlelected);
}
}
}
const { element, type } = this.searchResults[this.selectedSearchResultIndex];
if (element) {
// add focus to next item or title.
if (type === 'title') {
element.classList.add(SidebarSearch.CSS.sectionTitleSelected);
} else if (type === 'item') {
element.classList.add(SidebarSearch.CSS.sectionListItemSlelected);
}
// check if it's visible.
const rect = element.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',
});
}
}
// prevent default action.
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
}
filterSections(searchValue) {
// remove selection from previous search results.
if (this.selectedSearchResultIndex) {
const { element, type } = this.searchResults[this.selectedSearchResultIndex];
if (element) {
// remove focus from previous item or title.
if (type === 'title') {
element.classList.remove(SidebarSearch.CSS.sectionTitleSelected);
} else if (type === 'item') {
element.classList.remove(SidebarSearch.CSS.sectionListItemSlelected);
}
// empty selected index.
this.selectedSearchResultIndex = null;
}
}
// empty search results.
this.searchResults = [];
// match search value with sidebar sections.
this.sections.forEach(section => {
// match with section title.
const sectionTitle = section.querySelector('.' + SidebarSearch.CSS.sectionTitle);
let isTitleMatch = true;
const matchResults = [];
if (sectionTitle.innerText.trim().toLowerCase()
.indexOf(searchValue.toLowerCase()) === -1) {
isTitleMatch = false;
}
// match with section items.
const sectionList = section.querySelector('.' + SidebarSearch.CSS.sectionList);
let isItemMatch = false;
if (sectionList) {
const sectionListItems = sectionList.querySelectorAll('.' + SidebarSearch.CSS.sectionListItem);
sectionListItems.forEach(item => {
if (item.innerText.trim().toLowerCase()
.indexOf(searchValue.toLowerCase()) !== -1) {
// remove hiden class from item.
item.parentElement.classList.remove(SidebarSearch.CSS.sectionListItemWrapperHidden);
// add item to search results.
matchResults.push({
element: item,
type: 'item',
});
isItemMatch = true;
} else {
// hide item if it is not a match.
item.parentElement.classList.add(SidebarSearch.CSS.sectionListItemWrapperHidden);
}
});
}
if (!isTitleMatch && !isItemMatch) {
// hide section and it's items are not a match.
section.classList.add(SidebarSearch.CSS.sectionHidden);
} else {
// show section and it's items are a match.
section.classList.remove(SidebarSearch.CSS.sectionHidden);
// add section title to search results.
this.searchResults.push({
element: sectionTitle,
type: 'title',
}, ...matchResults);
}
});
}
}

View file

@ -1,6 +1,6 @@
import { Storage } from '../utils/storage';
import Shortcut from '@codexteam/shortcuts';
import SidebarSearch from '../classes/sidebar-search';
import SidebarFilter from '../classes/sidebar-filter';
/**
* Local storage key
*/
@ -69,8 +69,8 @@ export default class Sidebar {
// Sidebar visibility
this.isVisible = storedVisibility !== 'false';
// Sidebar search module
this.search = new SidebarSearch();
// Sidebar filter module
this.filter = new SidebarFilter();
}
/**
@ -90,7 +90,7 @@ export default class Sidebar {
this.nodes.slider.addEventListener('click', () => this.handleSliderClick());
this.nodes.search = moduleEl.querySelector('.' + Sidebar.CSS.sidebarSearch);
this.search.init(this.nodes.sections, this.nodes.sidebarContent, this.nodes.search);
this.filter.init(this.nodes.sections, this.nodes.sidebarContent, this.nodes.search);
this.ready();
}
@ -231,6 +231,7 @@ export default class Sidebar {
// make sidebar visible.
this.handleSliderClick();
}
// focus search input.
this.nodes.search.focus();
// Stop propagation of event.
e.stopPropagation();