diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a1212768c8b..510a841eded 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -98,6 +98,7 @@ This serves two purposes: - Moved the Vite build step to run before the site build to prevent duplicate media asset transfers in https://github.com/hydephp/develop/pull/2013 - Ported the HydeSearch plugin used for the documentation search to be an Alpine.js implementation in https://github.com/hydephp/develop/pull/2029 - Renamed Blade component `hyde::components.docs.search-widget` to `hyde::components.docs.search-modal` in https://github.com/hydephp/develop/pull/2029 + - Added support for customizing the search implementation by creating a `resources/js/HydeSearch.js` file in https://github.com/hydephp/develop/pull/2031 ### Deprecated diff --git a/packages/framework/resources/js/HydeSearch.js b/packages/framework/resources/js/HydeSearch.js new file mode 100644 index 00000000000..ff8a6591f94 --- /dev/null +++ b/packages/framework/resources/js/HydeSearch.js @@ -0,0 +1,78 @@ +function initHydeSearch(searchIndexUrl) { + return { + searchIndex: [], + searchTerm: '', + results: [], + isLoading: true, + statusMessage: '', + + async init() { + const response = await fetch(searchIndexUrl); + if (!response.ok) { + console.error('Could not load search index'); + return; + } + this.searchIndex = await response.json(); + this.isLoading = false; + }, + + search() { + const startTime = performance.now(); + this.results = []; + + if (!this.searchTerm) { + this.statusMessage = ''; + window.dispatchEvent(new CustomEvent('search-results-updated', { detail: { hasResults: false } })); + return; + } + + const searchResults = this.searchIndex.filter(entry => + entry.title.toLowerCase().includes(this.searchTerm.toLowerCase()) || + entry.content.toLowerCase().includes(this.searchTerm.toLowerCase()) + ); + + if (searchResults.length === 0) { + this.statusMessage = 'No results found.'; + window.dispatchEvent(new CustomEvent('search-results-updated', { detail: { hasResults: false } })); + return; + } + + const totalMatches = searchResults.reduce((acc, result) => { + return acc + (result.content.match(new RegExp(this.searchTerm, 'gi')) || []).length; + }, 0); + + searchResults.sort((a, b) => { + return (b.content.match(new RegExp(this.searchTerm, 'gi')) || []).length + - (a.content.match(new RegExp(this.searchTerm, 'gi')) || []).length; + }); + + this.results = searchResults.map(result => { + const matches = (result.content.match(new RegExp(this.searchTerm, 'gi')) || []).length; + const context = this.getSearchContext(result.content); + return { ...result, matches, context }; + }); + + const timeMs = Math.round((performance.now() - startTime) * 100) / 100; + this.statusMessage = `Found ${totalMatches} result${totalMatches !== 1 ? 's' : ''} in ${searchResults.length} pages. ~${timeMs}ms`; + + window.dispatchEvent(new CustomEvent('search-results-updated', { detail: { hasResults: true } })); + }, + + getSearchContext(content) { + const searchTermPos = content.toLowerCase().indexOf(this.searchTerm.toLowerCase()); + const sentenceStart = content.lastIndexOf('.', searchTermPos) + 1; + const sentenceEnd = content.indexOf('.', searchTermPos) + 1; + const sentence = content.substring(sentenceStart, sentenceEnd).trim(); + const template = document.getElementById('search-highlight-template'); + + return sentence.replace( + new RegExp(this.searchTerm, 'gi'), + match => { + const mark = template.content.querySelector('mark').cloneNode(); + mark.textContent = match; + return mark.outerHTML; + } + ); + } + }; +} diff --git a/packages/framework/resources/views/components/docs/hyde-search.blade.php b/packages/framework/resources/views/components/docs/hyde-search.blade.php index 08964cf60e7..85289f86dfd 100644 --- a/packages/framework/resources/views/components/docs/hyde-search.blade.php +++ b/packages/framework/resources/views/components/docs/hyde-search.blade.php @@ -1,6 +1,10 @@ @props(['modal' => true])