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

WIP: Added ToC, dynamic URLs, and search indexing #50257

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b30aae
Added tocbot for basic LHN
quinthar Oct 3, 2024
6e99abb
Make the Toc show top 3 levels by default
quinthar Oct 3, 2024
b9d9f5a
Working Toc with dynamic selection
quinthar Oct 4, 2024
3a24ea3
Added auto-updating of titles
quinthar Oct 4, 2024
7108c9b
Building search index locally, next to test in the Github Action
quinthar Oct 4, 2024
fbaa02a
Updating node version in build
quinthar Oct 4, 2024
94a6061
Stripping down extraneous bits from package.json
quinthar Oct 4, 2024
74bef71
Moving working directory to ./help for node
quinthar Oct 4, 2024
b35a978
Moving working directory to ./help for node
quinthar Oct 4, 2024
9290bf8
Moved search indexing to run after the site is generated
quinthar Oct 4, 2024
ef79c00
Added URL, title, and context to the index
quinthar Oct 4, 2024
0dc327e
Added basic search modal
quinthar Oct 4, 2024
202d7c7
Updating the styling a bit, make the search modal work a bit better
quinthar Oct 4, 2024
29532e2
Updated the styling a bit
quinthar Oct 4, 2024
efc5ef6
Added a spacer at the bottom
quinthar Oct 4, 2024
eb02cb1
Added scroll scrolling when choosing search result
quinthar Oct 4, 2024
41f1d2a
Added keyboard navigation of search results
quinthar Oct 4, 2024
68b42ca
Added highlight of search section upon click
quinthar Oct 4, 2024
f13dbd4
search modal improvements
quinthar Oct 5, 2024
dfc39f2
Prefixed all globals with g_ for clarity
quinthar Oct 5, 2024
064635b
Fixed some edge case search modal behaviors
quinthar Oct 5, 2024
dc67191
Tiny cleanup
quinthar Oct 5, 2024
4a15d03
Fiddling with indexing
quinthar Oct 5, 2024
a05766b
Fiddling with indexing
quinthar Oct 5, 2024
e90370b
Tons more content
quinthar Oct 5, 2024
792acda
wow, ChatGPT just... wrote a ton of docs
quinthar Oct 5, 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
11 changes: 11 additions & 0 deletions .github/workflows/deployNewHelp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ jobs:
bundler-cache: true
working-directory: ./help

# Install Node for _scripts/*.js
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20.15.1'

# Wil install the _help/package.js
- name: Install Node.js Dependencies
run: npm install
working-directory: ./help # Install the help dependencies, not App

# Manually run Jekyll, bypassing Github Pages
- name: Build Jekyll site
run: bundle exec jekyll build --source ./ --destination ./_site
Expand Down
337 changes: 337 additions & 0 deletions help/_includes/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
<div id="search-container">
<!-- Search Input styled as button -->
<input type="text" id="search-input-box" placeholder="Search..." />

<!-- Search Modal -->
<div id="search-modal" style="display:none;">
<div id="modal-content">
<span id="close-modal">&times;</span>
<input type="text" id="search-input" placeholder="Search..." />
<button id="search-submit">Search</button>

<!-- Search results container with tabindex to make it focusable -->
<div id="search-results" tabindex="0"></div>
</div>
</div>
</div>

<!-- Modal and Search Styling -->
<style>
/* Style for search input box (replaces button) */
#search-input-box {
padding: 8px 16px;
font-size: 16px;
cursor: pointer;
border: 1px solid #eaecef;
border-radius: 6px;
width: 100%;
box-sizing: border-box;
margin-bottom: 20px;
}

/* Modal background */
#search-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}

/* Modal content */
#modal-content {
background: white;
padding: 20px;
width: 60%;
max-width: 600px;
position: relative;
border-radius: 6px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}

/* Close button */
#close-modal {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
}

/* Search results */
#search-results {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
border-top: 1px solid #eaecef;
padding-top: 10px;
outline: none; /* Disable the strong outline */
}

.search-result {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eaecef;
}

.search-result-title {
font-weight: bold;
margin-bottom: 5px;
color: #0366d6;
text-decoration: none;
}

.search-result-title:hover {
text-decoration: underline;
}

.search-result-context {
font-size: 14px;
color: #586069;
}

/* Highlighted search result */
.highlight {
background-color: #eaecef;
}

/* Softer focus style for search results */
#search-results:focus {
border: 2px solid #ddd; /* Softer border on focus */
outline: none;
}

/* Soft yellow highlight for selected section */
.highlight-section {
background-color: #fffbcc;
transition: background-color 0.3s ease;
}
</style>

<!-- FlexSearch CDN -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/flexsearch.bundle.js"></script>

<!-- JavaScript for modal and search -->
<script>
// Keep track of the search results
let g_searchResultsArray = [];
let g_currentSelectionIndex = -1;
let g_highlightedSection = null;

// Declare the index variable globally so it can be reused
let g_index = null;

// Look up some commonly used elements once
const g_searchInputBox = document.getElementById('search-input-box');
const g_searchModal = document.getElementById('search-modal');
const g_closeModal = document.getElementById('close-modal');
const g_searchResults = document.getElementById('search-results');
const g_searchInput = document.getElementById('search-input');

// Show and initialize the search modal
function showSearchModal() {
g_searchModal.style.display = 'flex';
if (g_searchResultsArray.length > 0) {
g_searchResults.focus(); // Focus on search results if they exist
} else {
g_searchInput.focus();
}
}

// Open modal when search input is clicked
g_searchInputBox.addEventListener('click', showSearchModal);

// Open modal when Cmd+K is pressed
document.addEventListener('keydown', function(event) {
if (event.metaKey && event.key === 'k') {
event.preventDefault();
showSearchModal();
}
});

// Close modal when close button is clicked
g_closeModal.addEventListener('click', function() {
g_searchModal.style.display = 'none';
});

// Close modal when clicking outside of modal content
window.addEventListener('click', function(event) {
if (event.target == g_searchModal) {
g_searchModal.style.display = 'none';
}
});

// Handle keyboard navigation (arrow keys and enter)
g_searchResults.addEventListener('keydown', function(event) {
// If the modal is being shown and has active search results, capture keydown
if (g_searchModal.style.display === 'flex' && g_searchResultsArray.length > 0) {
if (event.key === 'ArrowDown') {
event.preventDefault();
selectNextResult();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
selectPreviousResult();
} else if (event.key === 'Enter' && g_currentSelectionIndex >= 0) {
event.preventDefault();
navigateToSelectedResult();
} else if (!['Tab', 'Shift'].includes(event.key)) {
g_searchInput.focus(); // Focus back on input if typing occurs
}
}
});

function selectNextResult() {
if (g_currentSelectionIndex < g_searchResultsArray.length - 1) {
g_currentSelectionIndex++;
updateSelectedResult();
}
}

function selectPreviousResult() {
if (g_currentSelectionIndex > 0) {
g_currentSelectionIndex--;
updateSelectedResult();
}
}

function updateSelectedResult() {
g_searchResultsArray.forEach((result, index) => {
if (index === g_currentSelectionIndex) {
result.classList.add('highlight');
result.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
result.classList.remove('highlight');
}
});
}

function navigateToSelectedResult() {
const selectedResult = g_searchResultsArray[g_currentSelectionIndex];
const link = selectedResult.querySelector('a');
if (link) {
link.click();
}
}

// Execute search when pressing "Enter" in the input field
g_searchInput.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
g_searchSubmit.click();
}
});

// Perform search when search button is clicked
const g_searchSubmit = document.getElementById('search-submit');

// Handle submitting the search query
g_searchSubmit.addEventListener('click', async function() {
const query = g_searchInput.value.trim().toLowerCase();
g_searchResults.innerHTML = '';
g_currentSelectionIndex = -1;

if (query.length === 0) {
g_searchResults.innerHTML = '<p>Please enter a search term.</p>';
return;
}

// Load the JSON search index file, if not already defined
if (g_index === null) {
console.log('Loading search index from:', '/searchIndex.json');
const response = await fetch('/searchIndex.json');
const indexData = await response.json();
g_index = new FlexSearch.Document({
document: {
id: 'id',
index: ['content'], // Index on the content field
store: ['title', 'url', 'content'], // Store title, URL, and content
}
});

// Import the index
for (const [key, data] of Object.entries(indexData)) {
await g_index.import(key, data);
}
} else {
console.log('Reusing existing search index');
}

// Perform search
const results = await g_index.search({
query,
field: 'content'
});

if (results.length > 0) {
g_searchResultsArray = [];
results.forEach(result => {
result.result.forEach(docId => {
const doc = g_index.store[docId];
if (doc && doc.content) {
const searchTermIndex = doc.content.toLowerCase().indexOf(query);
const contextBefore = doc.content.substring(Math.max(0, searchTermIndex - 30), searchTermIndex);
const contextAfter = doc.content.substring(searchTermIndex + query.length, Math.min(doc.content.length, searchTermIndex + query.length + 30));
const searchResultHtml = `
<div class="search-result">
<a href="${doc.url}" class="search-result-title" onclick="scrollToTOC('${doc.url}')">${doc.title}</a>
<div class="search-result-context">...${contextBefore}<strong>${query}</strong>${contextAfter}...</div>
</div>
`;
const resultElement = document.createElement('div');
resultElement.innerHTML = searchResultHtml;
g_searchResults.appendChild(resultElement);
g_searchResultsArray.push(resultElement);

// Automatically select the first result
if (g_searchResultsArray.length === 1) {
g_currentSelectionIndex = 0;
updateSelectedResult();
}
}
});
});
g_searchResults.focus(); // Focus on search results whenever there are results`
} else {
g_searchResults.innerHTML = '<p>No results found.</p>';
}
});

// Trigger the TOC link click to use TocBot's smooth scrolling behavior
function scrollToTOC(url) {
const elementId = url.split('#')[1];
const tocLink = document.querySelector(`.toc-sidebar a[href="#${elementId}"]`);

if (tocLink) {
tocLink.click(); // Simulate a click on the TOC link for smooth scroll
highlightSelectedSection(elementId); // Highlight the section in yellow
closeModalAfterClick();
}
}

// Highlight the selected section
function highlightSelectedSection(sectionId) {
// Remove the previous highlight, if any
if (g_highlightedSection) {
g_highlightedSection.classList.remove('highlight-section');
}

// Highlight the new section
const sectionElement = document.getElementById(sectionId);
if (sectionElement) {
sectionElement.classList.add('highlight-section');
g_highlightedSection = sectionElement;
}
}

// Close modal after clicking a search result
function closeModalAfterClick() {
g_searchModal.style.display = 'none';
}
</script>

Loading
Loading