diff --git a/src/theme/Root.js b/src/theme/Root.js new file mode 100644 index 00000000000..8ac35dff03b --- /dev/null +++ b/src/theme/Root.js @@ -0,0 +1,17 @@ +/** + * SWIZZLED VERSION: 2.4.3 + * REASONS: + * - Wrapped the component in context provider inorder to transfer data seamlessly between search bar and search page + */ + +/* eslint-disable */ +import React from 'react'; +import { SearchProvider } from '@site/src/utils/SearchContext'; + +export default function Root({ children }) { + return ( + +
{children}
+
+ ); +} diff --git a/src/theme/SearchPage/FilterDropdown.js b/src/theme/SearchPage/FilterDropdown.js new file mode 100644 index 00000000000..84f1fa01543 --- /dev/null +++ b/src/theme/SearchPage/FilterDropdown.js @@ -0,0 +1,177 @@ +/* eslint-disable */ +import React, { forwardRef, useEffect, useState } from 'react'; +import { + facetNamesJson, + allFacets, + subFilters, + subChildFilters, + buildSubFilters, + identitySubFilters, + maintainSubFilters, +} from '../../utils/searchConstant'; +import clsx from 'clsx'; +import styles from './styles.module.css'; +import { useAnnouncementBar } from '@docusaurus/theme-common/internal'; +import useIsBrowser from '@docusaurus/useIsBrowser'; + +export const FilterDropdown = forwardRef( + ({ selectedFacets, setSelectedFacets, styleProps = {} }, ref) => { + const { isActive: isAnnouncementBarActive } = useAnnouncementBar(); + const actualFacets = allFacets[1]; + const [isScrolled, setIsScrolled] = useState(false); + const isBrowser = useIsBrowser(); + + const handleCheckboxChange = (event) => { + const { value, checked } = event.target; + + // If selecting an individual item when "Select All" is checked, clear all and select only the clicked item + if (actualFacets.every((facet) => selectedFacets[1].includes(facet))) { + setSelectedFacets([selectedFacets[0], [value]]); + return; + } + + let updatedFacetList = checked + ? [...selectedFacets[1], value] + : selectedFacets[1].filter((facet) => facet !== value); + + if (!checked && updatedFacetList.length === 0) { + return; + } + + if (checked) { + // If a parent filter is checked, add its child filters + if (value === 'docusaurus_tag:docs-build-current') { + updatedFacetList = [ + ...new Set([...updatedFacetList, ...buildSubFilters]), + ]; + } else if (value === 'docusaurus_tag:docs-identity-rs-1-0-current') { + updatedFacetList = [ + ...new Set([...updatedFacetList, ...identitySubFilters]), + ]; + } else if (value === 'docusaurus_tag:docs-maintain-current') { + updatedFacetList = [ + ...new Set([...updatedFacetList, ...maintainSubFilters]), + ]; + } + } else { + // If a parent filter is unchecked, remove its child filters + if (value === 'docusaurus_tag:docs-build-current') { + updatedFacetList = updatedFacetList.filter( + (facet) => !buildSubFilters.includes(facet), + ); + } else if (value === 'docusaurus_tag:docs-identity-rs-1-0-current') { + updatedFacetList = updatedFacetList.filter( + (facet) => !identitySubFilters.includes(facet), + ); + } else if (value === 'docusaurus_tag:docs-maintain-current') { + updatedFacetList = updatedFacetList.filter( + (facet) => !maintainSubFilters.includes(facet), + ); + } + } + + setSelectedFacets([selectedFacets[0], updatedFacetList]); + }; + + const handleSelectAll = (event) => { + const checked = event.target.checked; + const updatedFacetList = checked ? actualFacets.slice() : []; + setSelectedFacets([selectedFacets[0], updatedFacetList]); + }; + + useEffect(() => { + // Check if all subfilters are selected, if yes, select the parent filter + const parentFilterStatus = { + 'docusaurus_tag:docs-maintain-current': maintainSubFilters.every( + (facet) => selectedFacets[1].includes(facet), + ), + 'docusaurus_tag:docs-build-current': buildSubFilters.every((facet) => + selectedFacets[1].includes(facet), + ), + 'docusaurus_tag:docs-identity-rs-1-0-current': identitySubFilters.every( + (facet) => selectedFacets[1].includes(facet), + ), + }; + + Object.keys(parentFilterStatus).forEach((parentFilter) => { + if ( + parentFilterStatus[parentFilter] && + !selectedFacets[1].includes(parentFilter) + ) { + setSelectedFacets((prevState) => [ + prevState[0], + [...prevState[1], parentFilter], + ]); + } + }); + }, [selectedFacets, setSelectedFacets]); + + useEffect(() => { + const handleScroll = () => { + if (isBrowser && window.scrollY > 28) { + setIsScrolled(true); + } + }; + handleScroll(); + }, [isBrowser && window.scrollY]); + + return ( +
+ + +
+ ); + }, +); diff --git a/src/theme/SearchPage/index.js b/src/theme/SearchPage/index.js new file mode 100644 index 00000000000..e0ec019795c --- /dev/null +++ b/src/theme/SearchPage/index.js @@ -0,0 +1,508 @@ +/** + * SWIZZLED VERSION: 2.4.3 + * REASONS: + * - Ejected to add search filters using facets + */ + +/* eslint-disable */ +/* eslint-disable jsx-a11y/no-autofocus */ +import React, { + useContext, + useEffect, + useReducer, + useRef, + useState, +} from 'react'; +import clsx from 'clsx'; +import algoliaSearchHelper from 'algoliasearch-helper'; +import algoliaSearch from 'algoliasearch/lite'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import Head from '@docusaurus/Head'; +import Link from '@docusaurus/Link'; +import { useAllDocsData } from '@docusaurus/plugin-content-docs/client'; +import { + HtmlClassNameProvider, + useEvent, + usePluralForm, + useSearchQueryString, +} from '@docusaurus/theme-common'; +import { useTitleFormatter } from '@docusaurus/theme-common/internal'; +import Translate, { translate } from '@docusaurus/Translate'; +import { + useAlgoliaThemeConfig, + useSearchResultUrlProcessor, +} from '@docusaurus/theme-search-algolia/client'; +import Layout from '@theme/Layout'; +import styles from './styles.module.css'; +import { SearchContext } from '@site/src/utils/SearchContext'; +import { FilterDropdown } from './FilterDropdown'; +// Very simple pluralization: probably good enough for now +function useDocumentsFoundPlural() { + const { selectMessage } = usePluralForm(); + return (count) => + selectMessage( + count, + translate( + { + id: 'theme.SearchPage.documentsFound.plurals', + description: + 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One document found|{count} documents found', + }, + { count }, + ), + ); +} +function useDocsSearchVersionsHelpers() { + const allDocsData = useAllDocsData(); + // State of the version select menus / algolia facet filters + // docsPluginId -> versionName map + const [searchVersions, setSearchVersions] = useState(() => + Object.entries(allDocsData).reduce( + (acc, [pluginId, pluginData]) => ({ + ...acc, + [pluginId]: pluginData.versions[0].name, + }), + {}, + ), + ); + // Set the value of a single select menu + const setSearchVersion = (pluginId, searchVersion) => + setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion })); + const versioningEnabled = Object.values(allDocsData).some( + (docsData) => docsData.versions.length > 1, + ); + return { + allDocsData, + versioningEnabled, + searchVersions, + setSearchVersion, + }; +} +// We want to display one select per versioned docs plugin instance +function SearchVersionSelectList({ docsSearchVersionsHelpers }) { + const versionedPluginEntries = Object.entries( + docsSearchVersionsHelpers.allDocsData, + ) + // Do not show a version select for unversioned docs plugin instances + .filter(([, docsData]) => docsData.versions.length > 1); + return ( +
+ {versionedPluginEntries.map(([pluginId, docsData]) => { + const labelPrefix = + versionedPluginEntries.length > 1 ? `${pluginId}: ` : ''; + return ( + + ); + })} +
+ ); +} +function SearchPageContent() { + const { + algolia: { appId, apiKey, indexName }, + } = useAlgoliaThemeConfig(); + const processSearchResultUrl = useSearchResultUrlProcessor(); + const documentsFoundPlural = useDocumentsFoundPlural(); + const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers(); + const [searchQuery, setSearchQuery] = useSearchQueryString(); + const [footerStyle, setFooterStyle] = useState({ + position: 'relative', + width: '100%', + bottom: '0%', + }); + + const { selectedFacets, setSelectedFacets } = useContext(SearchContext); + const initialSearchResultState = { + items: [], + query: null, + totalResults: null, + totalPages: null, + lastPage: null, + hasMore: null, + loading: null, + }; + const [searchResultState, searchResultStateDispatcher] = useReducer( + (prevState, data) => { + switch (data.type) { + case 'reset': { + return initialSearchResultState; + } + case 'loading': { + return { ...prevState, loading: true }; + } + case 'update': { + if (searchQuery !== data.value.query) { + return prevState; + } + return { + ...data.value, + items: + data.value.lastPage === 0 + ? data.value.items + : prevState.items.concat(data.value.items), + }; + } + case 'advance': { + const hasMore = prevState.totalPages > prevState.lastPage + 1; + return { + ...prevState, + lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage, + hasMore, + }; + } + default: + return prevState; + } + }, + initialSearchResultState, + ); + const algoliaClient = algoliaSearch(appId, apiKey); + const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, { + hitsPerPage: 15, + advancedSyntax: true, + disjunctiveFacets: ['language', 'docusaurus_tag'], + facetFilters: [...selectedFacets], + }); + algoliaHelper.on( + 'result', + ({ results: { query, hits, page, nbHits, nbPages } }) => { + if (query === '' || !Array.isArray(hits)) { + searchResultStateDispatcher({ type: 'reset' }); + return; + } + const sanitizeValue = (value) => + value.replace( + /algolia-docsearch-suggestion--highlight/g, + 'search-result-match', + ); + const items = hits.map( + ({ + url, + _highlightResult: { hierarchy }, + _snippetResult: snippet = {}, + }) => { + const titles = Object.keys(hierarchy).map((key) => + sanitizeValue(hierarchy[key].value), + ); + return { + title: titles.pop(), + url: processSearchResultUrl(url), + summary: snippet.content + ? `${sanitizeValue(snippet.content.value)}...` + : '', + breadcrumbs: titles, + }; + }, + ); + searchResultStateDispatcher({ + type: 'update', + value: { + items, + query, + totalResults: nbHits, + totalPages: nbPages, + lastPage: page, + hasMore: nbPages > page + 1, + loading: false, + }, + }); + }, + ); + const [loaderRef, setLoaderRef] = useState(null); + const prevY = useRef(0); + const observer = useRef( + ExecutionEnvironment.canUseIntersectionObserver && + new IntersectionObserver( + (entries) => { + const { + isIntersecting, + boundingClientRect: { y: currentY }, + } = entries[0]; + if (isIntersecting && prevY.current > currentY) { + searchResultStateDispatcher({ type: 'advance' }); + } + prevY.current = currentY; + }, + { threshold: 1 }, + ), + ); + const getTitle = () => + searchQuery + ? translate( + { + id: 'theme.SearchPage.existingResultsTitle', + message: 'Search results for "{query}"', + description: 'The search page title for non-empty query', + }, + { + query: searchQuery, + }, + ) + : translate({ + id: 'theme.SearchPage.emptyResultsTitle', + message: 'Search the documentation', + description: 'The search page title for empty query', + }); + const makeSearch = useEvent((page = 0) => { + Object.entries(docsSearchVersionsHelpers.searchVersions).forEach( + ([pluginId, searchVersion]) => { + algoliaHelper.addDisjunctiveFacetRefinement( + 'docusaurus_tag', + `docs-${pluginId}-${searchVersion}`, + ); + }, + ); + algoliaHelper.setQuery(searchQuery).setPage(page).search(); + }); + useEffect(() => { + if (!loaderRef) { + return undefined; + } + const currentObserver = observer.current; + if (currentObserver) { + currentObserver.observe(loaderRef); + return () => currentObserver.unobserve(loaderRef); + } + return () => true; + }, [loaderRef]); + useEffect(() => { + searchResultStateDispatcher({ type: 'reset' }); + if (searchQuery) { + searchResultStateDispatcher({ type: 'loading' }); + setTimeout(() => { + makeSearch(); + }, 300); + } + }, [ + searchQuery, + JSON.stringify(selectedFacets), + docsSearchVersionsHelpers.searchVersions, + makeSearch, + ]); + useEffect(() => { + if (!searchResultState.lastPage || searchResultState.lastPage === 0) { + return; + } + makeSearch(searchResultState.lastPage); + }, [makeSearch, searchResultState.lastPage]); + + useEffect(() => { + const handleResultState = () => { + if (searchResultState.loading || !searchResultState.totalResults) { + setFooterStyle({ position: 'absolute', width: '100%', bottom: '0%' }); + } else { + setFooterStyle({ position: 'relative', width: '100%', bottom: '0%' }); + } + }; + handleResultState(); + return () => handleResultState(); + }, [JSON.stringify(searchResultState)]); + return ( + + + {useTitleFormatter(getTitle())} + {/* + We should not index search pages + See https://github.com/facebook/docusaurus/pull/3233 + */} + + +
+
+

{getTitle()}

+ +
+
e.preventDefault()}> +
+ setSearchQuery(e.target.value)} + value={searchQuery} + autoComplete='off' + autoFocus + /> +
+ + {docsSearchVersionsHelpers.versioningEnabled && ( + + )} + +
+
+ {!!searchResultState.totalResults && + documentsFoundPlural(searchResultState.totalResults)} +
+ + +
+ {searchResultState.items.length > 0 ? ( +
+ {searchResultState.items.map( + ({ title, url, summary, breadcrumbs }, i) => ( +
+

+ +

+ + {breadcrumbs.length > 0 && ( + + )} + + {summary && ( +

+ )} +

+ ), + )} +
+ ) : ( + [ + searchQuery && !searchResultState.loading && ( +

+ + No results were found + +

+ ), + !!searchResultState.loading && ( +
+ ), + ] + )} + {searchResultState.hasMore && ( +
+ + Fetching new results... + +
+ )} +
+ + ); +} +export default function SearchPage() { + return ( + + + + ); +} diff --git a/src/theme/SearchPage/styles.module.css b/src/theme/SearchPage/styles.module.css new file mode 100644 index 00000000000..c87d1d8c302 --- /dev/null +++ b/src/theme/SearchPage/styles.module.css @@ -0,0 +1,188 @@ +.searchQueryInput, +.searchVersionInput { + border-radius: var(--ifm-global-radius); + border: 2px solid var(--ifm-toc-border-color); + font: var(--ifm-font-size-base) var(--ifm-font-family-base); + padding: 0.8rem; + width: 100%; + background: var(--docsearch-searchbox-focus-background); + color: var(--docsearch-text-color); + margin-bottom: 0.5rem; + transition: border var(--ifm-transition-fast) ease; +} + +.searchQueryInput:focus, +.searchVersionInput:focus { + border-color: var(--docsearch-primary-color); + outline: none; +} + +.searchQueryInput::placeholder { + color: var(--docsearch-muted-color); +} + +.searchResultsColumn { + font-size: 0.9rem; + font-weight: bold; +} + +.algoliaLogo { + max-width: 150px; +} + +.algoliaLogoPathFill { + fill: var(--ifm-font-color-base); +} + +.searchResultItem { + padding: 1rem 0; + border-bottom: 1px solid var(--ifm-toc-border-color); +} + +.searchResultItemHeading { + font-weight: 400; + margin-bottom: 0; +} + +.searchResultItemPath { + font-size: 0.8rem; + color: var(--ifm-color-content-secondary); + --ifm-breadcrumb-separator-size-multiplier: 1; +} + +.searchResultItemSummary { + margin: 0.5rem 0 0; + font-style: italic; +} + +@media only screen and (max-width: 996px) { + .searchQueryColumn { + max-width: 60% !important; + } + + .searchVersionColumn { + max-width: 40% !important; + } + + .searchResultsColumn { + max-width: 60% !important; + } + + .searchLogoColumn { + max-width: 40% !important; + padding-left: 0 !important; + } +} + +@media screen and (max-width: 576px) { + .searchQueryColumn { + max-width: 100% !important; + } + + .searchVersionColumn { + max-width: 100% !important; + padding-left: var(--ifm-spacing-horizontal) !important; + } +} + +.loadingSpinner { + width: 3rem; + height: 3rem; + border: 0.4em solid #eee; + border-top-color: var(--ifm-color-primary); + border-radius: 50%; + animation: loading-spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes loading-spin { + 100% { + transform: rotate(360deg); + } +} + +.loader { + margin-top: 2rem; +} + +:global(.search-result-match) { + color: var(--docsearch-hit-color); + background: rgb(255 215 142 / 25%); + padding: 0.09em 0; +} +.heading { + display: flex; + justify-content: space-between; + align-items: center; + width: 125%; +} +.dropdown { + position: absolute; + right: 22%; + top: 245%; + cursor: pointer; + font-size: 14px; + font-weight: var(--ifm-font-weight-semibold); +} +.activeAnnouncementBar { + top: 200%; +} +.isScrolled { + top: 250%; +} +.dropdownMenu { + color: var(--ifm-navbar-link-color); + width: 210px; + position: absolute; + padding: 15px; + height: 300px; + left: -35%; + top: 104%; +} +.dropdownMenu::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.dropdownMenu { + scrollbar-width: thin; +} + +.subFilters { + margin-left: 20px; +} +.subChildFilters { + margin-left: 35px; +} +@media (max-width: 996px) { + .dropdown { + position: absolute; + right: 665%; + top: 459%; + } + .activeAnnouncementBar { + top: 340%; + } + .isScrolled { + top: 470%; + } + .dropdownMenu { + left: 65%; + } +} + +@media (max-width: 770px) { + .dropdown { + position: absolute; + right: 396%; + top: 253%; + } + .activeAnnouncementBar { + top: 128%; + } + .dropdownMenu { + position: absolute; + top: 101%; + left: -4%; + } +} diff --git a/src/utils/SearchContext.js b/src/utils/SearchContext.js new file mode 100644 index 00000000000..e036c05ea85 --- /dev/null +++ b/src/utils/SearchContext.js @@ -0,0 +1,21 @@ +/* eslint-disable */ + +import React, { createContext, useState } from 'react'; +import { allFacets } from './searchConstant'; + +export const SearchContext = createContext(null); + +export const SearchProvider = ({ children }) => { + const [selectedFacets, setSelectedFacets] = useState(allFacets); + + return ( + + {children} + + ); +}; diff --git a/src/utils/searchConstant.ts b/src/utils/searchConstant.ts new file mode 100644 index 00000000000..df5e818a965 --- /dev/null +++ b/src/utils/searchConstant.ts @@ -0,0 +1,86 @@ +export const allFacets = [ + 'language:en', + [ + 'docusaurus_tag:docs-get-started-current', + 'docusaurus_tag:docs-learn-current', + 'docusaurus_tag:docs-build-current', + 'docusaurus_tag:docs-iota-sdk-1-0-current', + 'docusaurus_tag:docs-identity-rs-1-0-current', + 'docusaurus_tag:docs-identity-rs-1-1-current', + 'docusaurus_tag:docs-identity-rs-1-2-current', + 'docusaurus_tag:docs-stronghold-rs-1-1-current', + 'docusaurus_tag:docs-apis-current', + 'docusaurus_tag:docs-cli-wallet-1-0-current', + 'docusaurus_tag:docs-community-current', + 'docusaurus_tag:docs-isc-v1-0-0-rc-6-current', + 'docusaurus_tag:docs-iota-sandbox-current', + 'docusaurus_tag:docs-introduction-docs-stardust-current', + 'docusaurus_tag:docs-chronicle-1-0-rc-2-current', + 'docusaurus_tag:docs-maintain-current', + 'docusaurus_tag:docs-hornet-2-0-current', + 'docusaurus_tag:docs-wasp-v1-0-0-rc-6-current', + 'docusaurus_tag:default', + 'docusaurus_tag:docs-iota-tips-current', + ], +]; + +export const facetNamesJson = { + 'docusaurus_tag:docs-get-started-current': 'Get Started', + 'docusaurus_tag:docs-learn-current': 'Learn', + 'docusaurus_tag:default': 'Tutorial', + 'docusaurus_tag:docs-apis-current': 'Apis', + 'docusaurus_tag:docs-cli-wallet-1-0-current': 'Cli Wallet', + 'docusaurus_tag:docs-maintain-current': 'Maintain', + 'docusaurus_tag:docs-identity-rs-1-2-current': 'Identity.rs 1.2', + 'docusaurus_tag:docs-identity-rs-1-1-current': 'Identity.rs 1.1', + 'docusaurus_tag:docs-identity-rs-1-0-current': 'Identity.rs', + 'docusaurus_tag:docs-iota-sandbox-current': 'Iota', + 'docusaurus_tag:docs-build-current': 'Build', + 'docusaurus_tag:docs-isc-v1-0-0-rc-6-current': 'ISC', + 'docusaurus_tag:docs-stronghold-rs-1-1-current': 'Stronghold', + 'docusaurus_tag:docs-iota-core-1-0-current': 'Iota Core', + 'docusaurus_tag:docs-wasp-v1-0-0-rc-6-current': 'Wasp', + 'docusaurus_tag:docs-iota-sdk-1-0-current': 'Iota sdk', + 'docusaurus_tag:docs-hornet-2-0-current': 'Hornet', + 'docusaurus_tag:docs-introduction-docs-stardust-current': 'Stardust', + 'docusaurus_tag:docs-chronicle-1-0-rc-2-current': 'Chronicle', + 'docusaurus_tag:docs-community-current': 'Community', + 'docusaurus_tag:docs-iota-tips-current': 'Tips', +}; +export const subFilters = [ + 'Wasp', + 'Hornet', + 'Identity.rs', + 'Cli Wallet', + 'Apis', + 'Stronghold', + 'Iota sdk', + 'ISC', + 'Iota', + 'Stardust', + 'Chronicle', + 'Community', +]; +export const subChildFilters = ['Identity.rs 1.2', 'Identity.rs 1.1']; +export const buildSubFilters = [ + 'docusaurus_tag:docs-iota-sdk-1-0-current', + 'docusaurus_tag:docs-identity-rs-1-0-current', + 'docusaurus_tag:docs-identity-rs-1-2-current', + 'docusaurus_tag:docs-identity-rs-1-1-current', + 'docusaurus_tag:docs-stronghold-rs-1-1-current', + 'docusaurus_tag:docs-apis-current', + 'docusaurus_tag:docs-cli-wallet-1-0-current', + 'docusaurus_tag:docs-community-current', + 'docusaurus_tag:docs-isc-v1-0-0-rc-6-current', + 'docusaurus_tag:docs-iota-sandbox-current', + 'docusaurus_tag:docs-introduction-docs-stardust-current', + 'docusaurus_tag:docs-chronicle-1-0-rc-2-current', +]; +export const identitySubFilters = [ + 'docusaurus_tag:docs-identity-rs-1-2-current', + 'docusaurus_tag:docs-identity-rs-1-1-current', +]; +export const maintainSubFilters = [ + 'docusaurus_tag:docs-wasp-v1-0-0-rc-6-current', + 'docusaurus_tag:docs-hornet-2-0-current', +];