Skip to content

Commit 1f4e3d4

Browse files
authored
Merge pull request #2029 from hydephp/recreate-the-hydesearch-plugin-with-alpine
[2.x] Recreate the HydeSearch plugin with Alpine.js
2 parents 4575d7f + 5782c86 commit 1f4e3d4

File tree

11 files changed

+158
-67
lines changed

11 files changed

+158
-67
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ This serves two purposes:
9696
- The realtime compiler now only serves assets from the media source directory (`_media`), and no longer checks the site output directory (`_site/media`) in https://github.com/hydephp/develop/pull/2012
9797
- **Breaking:** Replaced `--run-dev` and `--run-prod` build command flags with a single `--run-vite` flag that uses Vite to build assets in https://github.com/hydephp/develop/pull/2013
9898
- 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
99+
- Ported the HydeSearch plugin used for the documentation search to be an Alpine.js implementation in https://github.com/hydephp/develop/pull/2029
100+
- Renamed Blade component `hyde::components.docs.search-widget` to `hyde::components.docs.search-modal` in https://github.com/hydephp/develop/pull/2029
99101

100102
### Deprecated
101103

@@ -119,6 +121,8 @@ This serves two purposes:
119121
- Removed `Hyde::siteMediaPath()` method replaced by `MediaFile::outputPath()` in https://github.com/hydephp/develop/pull/1911
120122
- Removed Laravel Mix as a dependency in https://github.com/hydephp/develop/pull/2010 (replaced with Vite)
121123
- **Breaking:** Removed `npm run prod` command (replaced with `npm run build`)
124+
- Removed CDN include for the HydeSearch plugin replaced by Alpine.js implementation in https://github.com/hydephp/develop/pull/2029
125+
- This also removes the `<x-hyde::docs.search-input />` and `<x-hyde::docs.search-scripts />` Blade components, replaced by the new `<x-hyde::docs.hyde-search />` component.
122126

123127
### Fixed
124128

_media/app.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/creating-content/documentation-pages.md

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,13 @@ If you set this to false, Hyde will match the directory structure of the source
351351

352352
### Introduction
353353

354-
The HydeSearch plugin adds a search feature to documentation pages. It consists of two parts, a search index generator that runs during the build command, and a frontend JavaScript plugin that adds the actual search widget.
354+
Hyde includes a built-in search feature for documentation pages powered by Alpine.js. It consists of two parts:
355+
1. A search index generator that runs during the build command
356+
2. An Alpine.js powered frontend that provides the search interface
355357

356-
>info Tip: The HydeSearch plugin is what powers the search feature on this site! Why not [try it out](search)?
358+
>info Tip: The search feature is what powers the search on this site! Why not [try it out](search)?
357359

358-
The search feature is enabled by default. You can disable it by removing the `DocumentationSearch` option from the Hyde `Features` config array.
360+
The search feature is enabled by default. You can disable it by removing the `DocumentationSearch` option from the Hyde `Features` config array:
359361

360362
```php
361363
// filepath: config/hyde.php
@@ -366,17 +368,27 @@ The search feature is enabled by default. You can disable it by removing the `Do
366368

367369
### Using the Search
368370

369-
The search works by generating a JSON search index which the JavaScript plugin loads asynchronously.
371+
The search works by generating a JSON search index which Alpine.js loads asynchronously. There are two ways to access the search:
370372

371-
Two ways to access the search are added, one is a full page search screen that will be saved to `docs/search.html`.
372-
373-
The second method is a button added to the documentation pages, similar to how Algolia DocSearch works. Opening it will open a modal with an integrated search screen. You can also open the dialog using the keyboard shortcut `/`.
373+
1. A full-page search screen at `docs/search.html`
374+
2. A modal dialog accessible via a button in the documentation pages (similar to Algolia DocSearch). You can also open this dialog using the keyboard shortcut `/`
374375

375376
>info The full page can be disabled by setting `create_search_page` to `false` in the `docs` config.
376377

378+
### Search Features
379+
380+
The search implementation includes:
381+
- Real-time search results as you type
382+
- Context highlighting of search terms
383+
- Match counting and search timing statistics
384+
- Dark mode support
385+
- Loading state indicators
386+
- Keyboard navigation support
387+
- Mobile-responsive design
388+
377389
### Hiding Pages from Indexing
378390

379-
If you have a large page on your documentation site, like a changelog, you may want to hide it from the search index. You can do this by adding the page identifier to the `exclude_from_search` array in the `docs` config, similar to how navigation menu items are hidden. The page will still be accessible as normal but will not be added to the search index JSON file.
391+
For large pages like changelogs, you may want to exclude them from the search index. Add the page identifier to the `exclude_from_search` array in the docs config:
380392

381393
```php
382394
// filepath: config/docs.php
@@ -385,9 +397,11 @@ If you have a large page on your documentation site, like a changelog, you may w
385397
]
386398
```
387399

400+
The page will remain accessible but won't appear in search results.
401+
388402
### Live Search with the Realtime Compiler
389403

390-
The Realtime Compiler that powers the `php hyde serve` command will automatically generate a fresh search index each time the browser requests it.
404+
When using `php hyde serve`, the Realtime Compiler automatically generates a fresh search index each time it's requested, ensuring your search results stay current during development.
391405

392406
## Automatic "Edit Page" Button
393407

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
@props(['modal' => true])
2+
3+
<div id="hyde-search" x-data="hydeSearch">
4+
<div class="relative">
5+
<input type="search" name="search" id="search-input" x-model="searchTerm" @input="search()" placeholder="Search..." autocomplete="off" autofocus
6+
{{ $attributes->merge(['class' => 'w-full rounded text-base leading-normal bg-gray-100 dark:bg-gray-700 py-2 px-3']) }}
7+
>
8+
9+
<div x-show="isLoading" class="absolute right-3 top-2.5">
10+
<div class="animate-spin h-5 w-5 border-2 border-gray-500 rounded-full border-t-transparent"></div>
11+
</div>
12+
</div>
13+
14+
<div x-show="searchTerm" class="mt-4">
15+
<p x-text="statusMessage" class="text-sm text-gray-600 dark:text-gray-400 mb-2 pb-2"></p>
16+
17+
<dl class="space-y-4 -mt-4 pl-2 -ml-2 {{ $modal ? 'max-h-[60vh] overflow-x-hidden overflow-y-auto' : '' }}">
18+
<template x-for="result in results" :key="result.slug">
19+
<div>
20+
<dt>
21+
<a :href="result.destination" x-text="result.title" class="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"></a><span class="text-sm text-gray-600 dark:text-gray-400" x-text="`, ${result.matches} occurrence${result.matches !== 1 ? 's' : ''} found.`"></span>
22+
</dt>
23+
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-300" x-html="result.context"></dd>
24+
</div>
25+
</template>
26+
</dl>
27+
</div>
28+
29+
<script>
30+
document.addEventListener('alpine:init', () => {
31+
Alpine.data('hydeSearch', () => ({
32+
searchIndex: [],
33+
searchTerm: '',
34+
results: [],
35+
isLoading: true,
36+
statusMessage: '',
37+
38+
async init() {
39+
const response = await fetch('{{ Hyde::relativeLink(\Hyde\Framework\Features\Documentation\DocumentationSearchIndex::outputPath()) }}');
40+
if (!response.ok) {
41+
console.error('Could not load search index');
42+
return;
43+
}
44+
this.searchIndex = await response.json();
45+
this.isLoading = false;
46+
},
47+
48+
search() {
49+
const startTime = performance.now();
50+
this.results = [];
51+
52+
if (!this.searchTerm) {
53+
this.statusMessage = '';
54+
window.dispatchEvent(new CustomEvent('search-results-updated', { detail: { hasResults: false } }));
55+
return;
56+
}
57+
58+
const searchResults = this.searchIndex.filter(entry =>
59+
entry.title.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
60+
entry.content.toLowerCase().includes(this.searchTerm.toLowerCase())
61+
);
62+
63+
if (searchResults.length === 0) {
64+
this.statusMessage = 'No results found.';
65+
window.dispatchEvent(new CustomEvent('search-results-updated', { detail: { hasResults: false } }));
66+
return;
67+
}
68+
69+
const totalMatches = searchResults.reduce((acc, result) => {
70+
return acc + (result.content.match(new RegExp(this.searchTerm, 'gi')) || []).length;
71+
}, 0);
72+
73+
searchResults.sort((a, b) => {
74+
return (b.content.match(new RegExp(this.searchTerm, 'gi')) || []).length
75+
- (a.content.match(new RegExp(this.searchTerm, 'gi')) || []).length;
76+
});
77+
78+
this.results = searchResults.map(result => {
79+
const matches = (result.content.match(new RegExp(this.searchTerm, 'gi')) || []).length;
80+
const context = this.getSearchContext(result.content);
81+
return { ...result, matches, context };
82+
});
83+
84+
const timeMs = Math.round((performance.now() - startTime) * 100) / 100;
85+
this.statusMessage = `Found ${totalMatches} result${totalMatches !== 1 ? 's' : ''} in ${searchResults.length} pages. ~${timeMs}ms`;
86+
87+
window.dispatchEvent(new CustomEvent('search-results-updated', { detail: { hasResults: true } }));
88+
},
89+
90+
getSearchContext(content) {
91+
const searchTermPos = content.toLowerCase().indexOf(this.searchTerm.toLowerCase());
92+
const sentenceStart = content.lastIndexOf('.', searchTermPos) + 1;
93+
const sentenceEnd = content.indexOf('.', searchTermPos) + 1;
94+
const sentence = content.substring(sentenceStart, sentenceEnd).trim();
95+
96+
return sentence.replace(
97+
new RegExp(this.searchTerm, 'gi'),
98+
match => `<mark class="bg-yellow-400 dark:bg-yellow-300">${match}</mark>`
99+
);
100+
}
101+
}));
102+
});
103+
</script>
104+
</div>

packages/framework/resources/views/components/docs/search-input.blade.php

Lines changed: 0 additions & 6 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
<button id="search-menu-button" x-on:click="searchWindowOpen = ! searchWindowOpen"
22
:title="searchWindowOpen ? 'Close search window' : 'Open search window'; $nextTick(() => { setTimeout(() => { document.getElementById('search-input').focus(); }); });"
3-
class="absolute right-4 top-4 mr-4 z-10 opacity-75 hover:opacity-100 hidden md:block"
4-
aria-label="Toggle search window">
3+
class="absolute right-4 top-4 mr-4 z-10 opacity-75 hover:opacity-100 hidden md:block" aria-label="Toggle search window">
54
<span x-show="! searchWindowOpen">
6-
Search <svg class="float-left mr-1 dark:fill-white" xmlns="http://www.w3.org/2000/svg" height="24"
7-
viewBox="0 0 24 24" width="24" role="presentation">
5+
Search <svg class="float-left mr-1 dark:fill-white" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" role="presentation">
86
<path d="M0 0h24v24H0z" fill="none"/>
97
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
108
</span>
@@ -24,30 +22,35 @@ class="block md:hidden fixed bottom-4 right-4 z-10 rounded-full p-2 opacity-75 h
2422
</svg>
2523
</button>
2624

27-
<div id="search-window-container" x-show="searchWindowOpen" x-cloak role="dialog"
28-
class="z-30 fixed top-0 left-0 w-screen h-screen flex flex-col items-center px-8 py-24 md:py-16">
25+
<div id="search-window-container" x-show="searchWindowOpen" x-cloak role="dialog" class="z-30 fixed top-0 left-0 w-screen h-screen flex flex-col items-center px-8 py-24 md:py-16">
2926
<aside x-on:click.away="searchWindowOpen = false" id="search-menu"
30-
class="prose dark:prose-invert bg-white dark:bg-gray-800 z-50 p-4 rounded-lg overflow-y-hidden min-h-[300px] max-h-[75vh] w-[70ch] max-w-full cursor-auto ">
31-
<header class="flex justify-between pb-3 mb-3 border-b dark:border-gray-700 md:hidden">
27+
class="bg-white dark:bg-gray-800 z-50 p-4 rounded-lg overflow-y-hidden
28+
min-h-[300px] max-h-[75vh] w-[70ch] max-w-full cursor-auto
29+
flex flex-col gap-4">
30+
31+
<header class="flex justify-between items-center border-b dark:border-gray-700 pb-3 md:hidden">
3232
<strong>Search the documentation site</strong>
33-
<button @click="searchWindowOpen = false" title="Close search window" class="opacity-75 hover:opacity-100"
34-
aria-label="Close search window">
33+
<button @click="searchWindowOpen = false" title="Close search window"
34+
class="opacity-75 hover:opacity-100" aria-label="Close search window">
3535
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
3636
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
3737
</svg>
3838
</button>
3939
</header>
40-
<div>
41-
<x-hyde::docs.search-input/>
40+
41+
<div class="flex-grow">
42+
<x-hyde::docs.hyde-search />
4243
</div>
43-
<footer class="mt-auto -mb-2 leading-4 text-center font-mono hidden sm:flex justify-center">
44+
45+
<footer x-data="{ hasResults: false }" @search-results-updated.window="hasResults = $event.detail.hasResults"
46+
class="prose dark:prose-invert text-center font-mono hidden sm:block"
47+
x-show="!hasResults">
4448
<small>
45-
Press <code><kbd title="Forward slash">/</kbd></code> to open search window.
46-
Use <code><kbd title="Escape key">esc</kbd></code> to close.
49+
Press <code class="p-0"><kbd title="Forward slash" class="shadow-none">/</kbd></code> to open search window.
50+
Use <code class="p-0"><kbd title="Escape key" class="shadow-none">esc</kbd></code> to close.
4751
</small>
4852
</footer>
4953
</aside>
5054

51-
<div id="search-window-backdrop" title="Click to close search window"
52-
class="w-screen h-screen cursor-pointer z-40 bg-black/50 absolute top-0"></div>
55+
<div id="search-window-backdrop" title="Click to close search window" class="w-screen h-screen cursor-pointer z-40 bg-black/50 absolute top-0"></div>
5356
</div>

packages/framework/resources/views/components/docs/search-scripts.blade.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/framework/resources/views/layouts/docs.blade.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
@include('hyde::components.docs.sidebar-backdrop')
1919

2020
@if(Hyde\Facades\Features::hasDocumentationSearch())
21-
@include('hyde::components.docs.search-widget')
22-
@include('hyde::components.docs.search-scripts')
21+
@include('hyde::components.docs.search-modal')
2322
@endif
2423
</div>
2524

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
@extends('hyde::layouts.docs')
22
@section('content')
33
<h1>Search the documentation site</h1>
4-
<style>#search-menu-button, .edit-page-link {
5-
display: none !important;
6-
}
4+
<style>#search-menu-button, .edit-page-link { display: none !important; }</style>
75

8-
#search-results {
9-
max-height: unset !important;
10-
}</style>
11-
<x-hyde::docs.search-input class="max-w-xs border-b-4 border-indigo-400"/>
12-
@endsection
6+
<div class="not-prose">
7+
<x-hyde::docs.hyde-search class="max-w-sm" :modal="false" />
8+
</div>
9+
@endsection

packages/hydefront/sass/docs/search.scss

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)