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

[2.x] Recreate the HydeSearch plugin with Alpine.js #2029

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c9db18c
Begin recreating the HydeSearch plugin with Alpine
caendesilva Nov 16, 2024
4772fd1
Delete now unused `search.scss` file
caendesilva Nov 16, 2024
1db8107
Add top margin
caendesilva Nov 16, 2024
c6a2c77
Hide horizontal overflow
caendesilva Nov 16, 2024
b903795
Tweak colors
caendesilva Nov 16, 2024
531c4a0
Remove the debounce
caendesilva Nov 16, 2024
8089425
Use indigo styles
caendesilva Nov 16, 2024
4d739f2
Remove prose style
caendesilva Nov 16, 2024
96bc412
Fix spacing issue
caendesilva Nov 16, 2024
cabd5a4
Tweak styles
caendesilva Nov 16, 2024
fe4cdb9
Tweak margin
caendesilva Nov 16, 2024
cf17e40
Increase contrast
caendesilva Nov 16, 2024
835fcea
Revert "Tweak margin"
caendesilva Nov 16, 2024
fc9e2c0
Compile assets
caendesilva Nov 16, 2024
0e46a13
Update RELEASE_NOTES.md
caendesilva Nov 16, 2024
cf0dc9b
Merge HydeSearch Blade views into single component
caendesilva Nov 16, 2024
dbf8cde
Remove noscript tag from search plugin
caendesilva Nov 16, 2024
0c591b2
Format Blade
caendesilva Nov 16, 2024
7c811e0
Manually reformat Blade
caendesilva Nov 16, 2024
cf42f2a
Cleanup Blade
caendesilva Nov 16, 2024
84d6537
Simplify modal layout styles
caendesilva Nov 16, 2024
73e9067
Hide search footer when there are results
caendesilva Nov 16, 2024
93bd9b9
Tweak margins
caendesilva Nov 16, 2024
5cefc0b
Add padding and negative margin to get nicer focus states
caendesilva Nov 16, 2024
fa1037b
Move font weight rule to heading
caendesilva Nov 16, 2024
ffee3b3
Update and clean up the search feature documentation
caendesilva Nov 16, 2024
8ebf91d
Tweak full page search styles
caendesilva Nov 16, 2024
b592ae0
Fix not prose styles
caendesilva Nov 16, 2024
cc796dd
Rename search widget component to search modal
caendesilva Nov 16, 2024
5782c86
Fix Blade formatting
caendesilva Nov 16, 2024
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
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ This serves two purposes:
- 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
- **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
- 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

### Deprecated

Expand All @@ -119,6 +121,8 @@ This serves two purposes:
- Removed `Hyde::siteMediaPath()` method replaced by `MediaFile::outputPath()` in https://github.com/hydephp/develop/pull/1911
- Removed Laravel Mix as a dependency in https://github.com/hydephp/develop/pull/2010 (replaced with Vite)
- **Breaking:** Removed `npm run prod` command (replaced with `npm run build`)
- Removed CDN include for the HydeSearch plugin replaced by Alpine.js implementation in https://github.com/hydephp/develop/pull/2029
- 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.

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion _media/app.css

Large diffs are not rendered by default.

32 changes: 23 additions & 9 deletions docs/creating-content/documentation-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,13 @@ If you set this to false, Hyde will match the directory structure of the source

### Introduction

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.
Hyde includes a built-in search feature for documentation pages powered by Alpine.js. It consists of two parts:
1. A search index generator that runs during the build command
2. An Alpine.js powered frontend that provides the search interface

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

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

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

### Using the Search

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

Two ways to access the search are added, one is a full page search screen that will be saved to `docs/search.html`.

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 `/`.
1. A full-page search screen at `docs/search.html`
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 `/`

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

### Search Features

The search implementation includes:
- Real-time search results as you type
- Context highlighting of search terms
- Match counting and search timing statistics
- Dark mode support
- Loading state indicators
- Keyboard navigation support
- Mobile-responsive design

### Hiding Pages from Indexing

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.
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:

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

The page will remain accessible but won't appear in search results.

### Live Search with the Realtime Compiler

The Realtime Compiler that powers the `php hyde serve` command will automatically generate a fresh search index each time the browser requests it.
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.

## Automatic "Edit Page" Button

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
@props(['modal' => true])

<div id="hyde-search" x-data="hydeSearch">
<div class="relative">
<input type="search" name="search" id="search-input" x-model="searchTerm" @input="search()" placeholder="Search..." autocomplete="off" autofocus
{{ $attributes->merge(['class' => 'w-full rounded text-base leading-normal bg-gray-100 dark:bg-gray-700 py-2 px-3']) }}
>

<div x-show="isLoading" class="absolute right-3 top-2.5">
<div class="animate-spin h-5 w-5 border-2 border-gray-500 rounded-full border-t-transparent"></div>
</div>
</div>

<div x-show="searchTerm" class="mt-4">
<p x-text="statusMessage" class="text-sm text-gray-600 dark:text-gray-400 mb-2 pb-2"></p>

<dl class="space-y-4 -mt-4 pl-2 -ml-2 {{ $modal ? 'max-h-[60vh] overflow-x-hidden overflow-y-auto' : '' }}">
<template x-for="result in results" :key="result.slug">
<div>
<dt>
<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>
</dt>
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-300" x-html="result.context"></dd>
</div>
</template>
</dl>
</div>

<script>
document.addEventListener('alpine:init', () => {
Alpine.data('hydeSearch', () => ({
searchIndex: [],
searchTerm: '',
results: [],
isLoading: true,
statusMessage: '',

async init() {
const response = await fetch('{{ Hyde::relativeLink(\Hyde\Framework\Features\Documentation\DocumentationSearchIndex::outputPath()) }}');
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();

return sentence.replace(
new RegExp(this.searchTerm, 'gi'),
match => `<mark class="bg-yellow-400 dark:bg-yellow-300">${match}</mark>`
);
}
}));
});
</script>
</div>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<button id="search-menu-button" x-on:click="searchWindowOpen = ! searchWindowOpen"
:title="searchWindowOpen ? 'Close search window' : 'Open search window'; $nextTick(() => { setTimeout(() => { document.getElementById('search-input').focus(); }); });"
class="absolute right-4 top-4 mr-4 z-10 opacity-75 hover:opacity-100 hidden md:block"
aria-label="Toggle search window">
class="absolute right-4 top-4 mr-4 z-10 opacity-75 hover:opacity-100 hidden md:block" aria-label="Toggle search window">
<span x-show="! searchWindowOpen">
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">
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">
<path d="M0 0h24v24H0z" fill="none"/>
<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>
</span>
Expand All @@ -24,30 +22,35 @@ class="block md:hidden fixed bottom-4 right-4 z-10 rounded-full p-2 opacity-75 h
</svg>
</button>

<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">
<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">
<aside x-on:click.away="searchWindowOpen = false" id="search-menu"
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 ">
<header class="flex justify-between pb-3 mb-3 border-b dark:border-gray-700 md:hidden">
class="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
flex flex-col gap-4">

<header class="flex justify-between items-center border-b dark:border-gray-700 pb-3 md:hidden">
<strong>Search the documentation site</strong>
<button @click="searchWindowOpen = false" title="Close search window" class="opacity-75 hover:opacity-100"
aria-label="Close search window">
<button @click="searchWindowOpen = false" title="Close search window"
class="opacity-75 hover:opacity-100" aria-label="Close search window">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<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>
</svg>
</button>
</header>
<div>
<x-hyde::docs.search-input/>

<div class="flex-grow">
<x-hyde::docs.hyde-search />
</div>
<footer class="mt-auto -mb-2 leading-4 text-center font-mono hidden sm:flex justify-center">

<footer x-data="{ hasResults: false }" @search-results-updated.window="hasResults = $event.detail.hasResults"
class="prose dark:prose-invert text-center font-mono hidden sm:block"
x-show="!hasResults">
<small>
Press <code><kbd title="Forward slash">/</kbd></code> to open search window.
Use <code><kbd title="Escape key">esc</kbd></code> to close.
Press <code class="p-0"><kbd title="Forward slash" class="shadow-none">/</kbd></code> to open search window.
Use <code class="p-0"><kbd title="Escape key" class="shadow-none">esc</kbd></code> to close.
</small>
</footer>
</aside>

<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>
<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>
</div>

This file was deleted.

3 changes: 1 addition & 2 deletions packages/framework/resources/views/layouts/docs.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
@include('hyde::components.docs.sidebar-backdrop')

@if(Hyde\Facades\Features::hasDocumentationSearch())
@include('hyde::components.docs.search-widget')
@include('hyde::components.docs.search-scripts')
@include('hyde::components.docs.search-modal')
@endif
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
@extends('hyde::layouts.docs')
@section('content')
<h1>Search the documentation site</h1>
<style>#search-menu-button, .edit-page-link {
display: none !important;
}
<style>#search-menu-button, .edit-page-link { display: none !important; }</style>

#search-results {
max-height: unset !important;
}</style>
<x-hyde::docs.search-input class="max-w-xs border-b-4 border-indigo-400"/>
@endsection
<div class="not-prose">
<x-hyde::docs.hyde-search class="max-w-sm" :modal="false" />
</div>
@endsection
13 changes: 0 additions & 13 deletions packages/hydefront/sass/docs/search.scss

This file was deleted.

2 changes: 0 additions & 2 deletions packages/hydefront/sass/hyde-docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
* HydeDocs Core CSS
*/

@use 'docs/search';

html {
scroll-behavior: smooth;
}