mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
adding event tracking to site search (#1872)
This commit is contained in:
@@ -1,12 +1,24 @@
|
|||||||
// Search data
|
// Search data
|
||||||
const version = document.documentElement.getAttribute('data-version') || '';
|
const version = document.documentElement.getAttribute('data-version') || '';
|
||||||
const res = await Promise.all([import('https://cdn.jsdelivr.net/npm/lunr/+esm'), fetch(`/search.json?v=${version}`)]);
|
const res = await Promise.all([
|
||||||
|
import('https://cdn.jsdelivr.net/npm/lunr/+esm'),
|
||||||
|
fetch(`/search.json?v=${version}`),
|
||||||
|
import('/assets/scripts/track.js').catch(() => null),
|
||||||
|
]);
|
||||||
const lunr = res[0].default;
|
const lunr = res[0].default;
|
||||||
const searchData = await res[1].json();
|
const searchData = await res[1].json();
|
||||||
const searchIndex = lunr.Index.load(searchData.searchIndex);
|
const searchIndex = lunr.Index.load(searchData.searchIndex);
|
||||||
const map = searchData.map;
|
const map = searchData.map;
|
||||||
const searchDebounce = 200;
|
const searchDebounce = 200;
|
||||||
|
const queryTrackDelay = 1000;
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
|
let queryTrackTimeout;
|
||||||
|
let lastTrackedQuery = '';
|
||||||
|
let resultSelected = false;
|
||||||
|
|
||||||
|
// Optional event tracking - works standalone if track.js isn't available
|
||||||
|
const trackModule = res[2];
|
||||||
|
const trackEvent = trackModule?.trackEvent || window.trackEvent || (() => {});
|
||||||
|
|
||||||
// We're using Turbo, so references to these elements aren't guaranteed to remain intact
|
// We're using Turbo, so references to these elements aren't guaranteed to remain intact
|
||||||
function getElements() {
|
function getElements() {
|
||||||
@@ -17,6 +29,24 @@ function getElements() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackQuerySubmit(query, resultSelectedValue) {
|
||||||
|
if (!query || query.length === 0) return;
|
||||||
|
|
||||||
|
const { results } = getElements();
|
||||||
|
if (!results) return;
|
||||||
|
|
||||||
|
const matches = results.querySelectorAll('li').length;
|
||||||
|
const truncatedQuery = query.length > 500 ? query.substring(0, 500) : query;
|
||||||
|
|
||||||
|
trackEvent('navigation:search_query_submit', {
|
||||||
|
query: truncatedQuery,
|
||||||
|
query_length: query.length,
|
||||||
|
result_count: matches,
|
||||||
|
has_results: matches > 0,
|
||||||
|
result_selected: resultSelectedValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element
|
// Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element
|
||||||
document.addEventListener('keydown', event => {
|
document.addEventListener('keydown', event => {
|
||||||
if (
|
if (
|
||||||
@@ -42,40 +72,98 @@ document.addEventListener('click', event => {
|
|||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
const { dialog, input, results } = getElements();
|
const { dialog, input, results } = getElements();
|
||||||
|
if (!dialog || !input || !results) return;
|
||||||
|
|
||||||
|
const wasAlreadyOpen = dialog.open;
|
||||||
|
|
||||||
|
// Remove existing listeners before adding to prevent duplicates
|
||||||
|
input.removeEventListener('input', handleInput);
|
||||||
|
results.removeEventListener('click', handleSelection);
|
||||||
|
dialog.removeEventListener('keydown', handleKeyDown);
|
||||||
|
dialog.removeEventListener('wa-hide', handleClose);
|
||||||
|
resultSelected = false;
|
||||||
|
lastTrackedQuery = '';
|
||||||
input.addEventListener('input', handleInput);
|
input.addEventListener('input', handleInput);
|
||||||
results.addEventListener('click', handleSelection);
|
results.addEventListener('click', handleSelection);
|
||||||
dialog.addEventListener('keydown', handleKeyDown);
|
dialog.addEventListener('keydown', handleKeyDown);
|
||||||
dialog.addEventListener('wa-hide', handleClose);
|
dialog.addEventListener('wa-hide', handleClose);
|
||||||
dialog.open = true;
|
dialog.open = true;
|
||||||
|
if (!wasAlreadyOpen) {
|
||||||
|
trackEvent('navigation:search_dialog_open');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function cleanup() {
|
||||||
const { dialog, input, results } = getElements();
|
const { dialog, input, results } = getElements();
|
||||||
|
if (!dialog || !input || !results) return;
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
clearTimeout(queryTrackTimeout);
|
||||||
input.removeEventListener('input', handleInput);
|
input.removeEventListener('input', handleInput);
|
||||||
results.removeEventListener('click', handleSelection);
|
results.removeEventListener('click', handleSelection);
|
||||||
dialog.removeEventListener('keydown', handleKeyDown);
|
dialog.removeEventListener('keydown', handleKeyDown);
|
||||||
dialog.removeEventListener('wa-hide', handleClose);
|
dialog.removeEventListener('wa-hide', handleClose);
|
||||||
dialog.open = false;
|
|
||||||
|
// Reset state to prevent leakage between dialog sessions
|
||||||
|
resultSelected = false;
|
||||||
|
lastTrackedQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
async function handleClose() {
|
||||||
const { input } = getElements();
|
const { dialog, input } = getElements();
|
||||||
|
if (!dialog || !input) return;
|
||||||
|
clearTimeout(queryTrackTimeout);
|
||||||
|
queryTrackTimeout = null;
|
||||||
|
dialog.removeEventListener('wa-hide', handleClose);
|
||||||
|
if (!resultSelected) {
|
||||||
|
const query = input.value.trim();
|
||||||
|
if (query.length > 0 && query !== lastTrackedQuery) {
|
||||||
|
trackQuerySubmit(query, false);
|
||||||
|
lastTrackedQuery = query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input.value = '';
|
input.value = '';
|
||||||
updateResults();
|
try {
|
||||||
|
await updateResults();
|
||||||
|
} catch (error) {
|
||||||
|
// Silently handle errors - UI cleanup should continue
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
trackEvent('navigation:search_dialog_close');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput() {
|
function handleInput() {
|
||||||
const { input } = getElements();
|
const { input } = getElements();
|
||||||
|
if (!input) return;
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
|
clearTimeout(queryTrackTimeout);
|
||||||
|
|
||||||
|
const query = input.value.trim();
|
||||||
|
|
||||||
|
if (query.length === 0) {
|
||||||
|
lastTrackedQuery = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
await updateResults(query);
|
||||||
|
if (query.length > 0 && query !== lastTrackedQuery) {
|
||||||
|
queryTrackTimeout = setTimeout(() => {
|
||||||
|
const { input: currentInput, results } = getElements();
|
||||||
|
if (!currentInput || resultSelected) return;
|
||||||
|
|
||||||
|
const currentQuery = currentInput.value.trim();
|
||||||
|
if (currentQuery === query && currentQuery !== lastTrackedQuery) {
|
||||||
|
trackQuerySubmit(currentQuery, false);
|
||||||
|
lastTrackedQuery = currentQuery;
|
||||||
|
}
|
||||||
|
}, queryTrackDelay);
|
||||||
|
}
|
||||||
|
}, searchDebounce);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
const { input, results } = getElements();
|
const { input, results } = getElements();
|
||||||
|
if (!input || !results) return;
|
||||||
|
|
||||||
// Handle keyboard selections
|
// Handle keyboard selections
|
||||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
|
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
|
||||||
@@ -104,7 +192,12 @@ function handleKeyDown(event) {
|
|||||||
nextEl = items[items.length - 1];
|
nextEl = items[items.length - 1];
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
currentEl?.querySelector('a')?.click();
|
if (currentEl) {
|
||||||
|
const link = currentEl.querySelector('a');
|
||||||
|
if (link) {
|
||||||
|
selectResult(link, 'keyboard_enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,27 +214,62 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectResult(link, selectionMethod) {
|
||||||
|
const { input, results } = getElements();
|
||||||
|
if (!input || !link) return;
|
||||||
|
|
||||||
|
// Clear pending query tracking timeout to prevent duplicate events
|
||||||
|
clearTimeout(queryTrackTimeout);
|
||||||
|
queryTrackTimeout = null;
|
||||||
|
resultSelected = true; // Set immediately so timeout callback (if executing) sees it
|
||||||
|
|
||||||
|
const query = input.value.trim();
|
||||||
|
if (!link.dataset.searchResultIndex) return;
|
||||||
|
const resultIndex = parseInt(link.dataset.searchResultIndex, 10);
|
||||||
|
if (isNaN(resultIndex) || resultIndex < 1) return;
|
||||||
|
|
||||||
|
const resultUrl = link.dataset.searchResultUrl || link.getAttribute('href');
|
||||||
|
if (!resultUrl) return;
|
||||||
|
lastTrackedQuery = query;
|
||||||
|
trackQuerySubmit(query, true);
|
||||||
|
trackEvent('navigation:search_result_click', {
|
||||||
|
query,
|
||||||
|
result_index: resultIndex,
|
||||||
|
result_url: resultUrl,
|
||||||
|
selection_method: selectionMethod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dialog } = getElements();
|
||||||
|
if (dialog) {
|
||||||
|
dialog.removeEventListener('wa-hide', handleClose);
|
||||||
|
cleanup();
|
||||||
|
trackEvent('navigation:search_dialog_close');
|
||||||
|
dialog.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.Turbo) {
|
||||||
|
Turbo.visit(resultUrl);
|
||||||
|
} else {
|
||||||
|
location.href = resultUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleSelection(event) {
|
function handleSelection(event) {
|
||||||
const link = event.target.closest('a');
|
const link = event.target.closest('a');
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
hide();
|
selectResult(link, 'mouse_click');
|
||||||
|
|
||||||
if (window.Turbo) {
|
|
||||||
Turbo.visit(link.href);
|
|
||||||
} else {
|
|
||||||
location.href = link.href;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queries the search index and updates the results
|
// Queries the search index and updates the results
|
||||||
async function updateResults(query = '') {
|
async function updateResults(query = '') {
|
||||||
const { dialog, input, results } = getElements();
|
const { dialog, input, results } = getElements();
|
||||||
|
if (!dialog || !input || !results) return;
|
||||||
try {
|
try {
|
||||||
const hasQuery = query.length > 0;
|
const trimmedQuery = query.trim();
|
||||||
|
const hasQuery = trimmedQuery.length > 0;
|
||||||
let matches = [];
|
let matches = [];
|
||||||
|
|
||||||
if (hasQuery) {
|
if (hasQuery) {
|
||||||
@@ -149,13 +277,13 @@ async function updateResults(query = '') {
|
|||||||
const seenRefs = new Set();
|
const seenRefs = new Set();
|
||||||
|
|
||||||
// Start with a standard search to get the best "exact match" result
|
// Start with a standard search to get the best "exact match" result
|
||||||
searchIndex.search(`${query}`).forEach(match => {
|
searchIndex.search(`${trimmedQuery}`).forEach(match => {
|
||||||
matches.push(match);
|
matches.push(match);
|
||||||
seenRefs.add(match.ref);
|
seenRefs.add(match.ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add wildcard matches if not already included
|
// Add wildcard matches if not already included
|
||||||
searchIndex.search(`${query}*`).forEach(match => {
|
searchIndex.search(`${trimmedQuery}*`).forEach(match => {
|
||||||
if (!seenRefs.has(match.ref)) {
|
if (!seenRefs.has(match.ref)) {
|
||||||
matches.push(match);
|
matches.push(match);
|
||||||
seenRefs.add(match.ref);
|
seenRefs.add(match.ref);
|
||||||
@@ -163,11 +291,10 @@ async function updateResults(query = '') {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add fuzzy search matches last
|
// Add fuzzy search matches last
|
||||||
const fuzzyTokens = query
|
const fuzzyTokens = trimmedQuery
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map(term => `${term}~1`)
|
.map(term => `${term}~1`)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
searchIndex.search(fuzzyTokens).forEach(match => {
|
searchIndex.search(fuzzyTokens).forEach(match => {
|
||||||
if (!seenRefs.has(match.ref)) {
|
if (!seenRefs.has(match.ref)) {
|
||||||
matches.push(match);
|
matches.push(match);
|
||||||
@@ -180,12 +307,12 @@ async function updateResults(query = '') {
|
|||||||
|
|
||||||
dialog.classList.toggle('has-results', hasQuery && hasResults);
|
dialog.classList.toggle('has-results', hasQuery && hasResults);
|
||||||
dialog.classList.toggle('no-results', hasQuery && !hasResults);
|
dialog.classList.toggle('no-results', hasQuery && !hasResults);
|
||||||
|
|
||||||
input.setAttribute('aria-activedescendant', '');
|
input.setAttribute('aria-activedescendant', '');
|
||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
|
|
||||||
matches.forEach((match, index) => {
|
matches.forEach((match, index) => {
|
||||||
const page = map[match.ref];
|
const page = map[match.ref];
|
||||||
|
if (!page || !page.url) return;
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
const displayTitle = page.title ?? '';
|
const displayTitle = page.title ?? '';
|
||||||
@@ -197,12 +324,10 @@ async function updateResults(query = '') {
|
|||||||
li.setAttribute('role', 'option');
|
li.setAttribute('role', 'option');
|
||||||
li.setAttribute('id', `search-result-item-${match.ref}`);
|
li.setAttribute('id', `search-result-item-${match.ref}`);
|
||||||
li.setAttribute('data-selected', index === 0 ? 'true' : 'false');
|
li.setAttribute('data-selected', index === 0 ? 'true' : 'false');
|
||||||
|
|
||||||
if (page.url === '/') icon = 'home';
|
if (page.url === '/') icon = 'home';
|
||||||
if (page.url.startsWith('/docs/utilities/native')) icon = 'code';
|
if (page.url.startsWith('/docs/utilities/native')) icon = 'code';
|
||||||
if (page.url.startsWith('/docs/components')) icon = 'puzzle-piece';
|
if (page.url.startsWith('/docs/components')) icon = 'puzzle-piece';
|
||||||
if (page.url.startsWith('/docs/theme') || page.url.startsWith('/docs/restyle')) icon = 'palette';
|
if (page.url.startsWith('/docs/theme') || page.url.startsWith('/docs/restyle')) icon = 'palette';
|
||||||
|
|
||||||
a.href = page.url;
|
a.href = page.url;
|
||||||
a.innerHTML = `
|
a.innerHTML = `
|
||||||
<div class="site-search-result-icon" aria-hidden="true">
|
<div class="site-search-result-icon" aria-hidden="true">
|
||||||
@@ -218,6 +343,9 @@ async function updateResults(query = '') {
|
|||||||
a.querySelector('.site-search-result-description').textContent = displayDescription;
|
a.querySelector('.site-search-result-description').textContent = displayDescription;
|
||||||
a.querySelector('.site-search-result-url').textContent = displayUrl;
|
a.querySelector('.site-search-result-url').textContent = displayUrl;
|
||||||
|
|
||||||
|
// Use 1-based indexing for analytics
|
||||||
|
a.dataset.searchResultIndex = (index + 1).toString();
|
||||||
|
a.dataset.searchResultUrl = page.url;
|
||||||
li.appendChild(a);
|
li.appendChild(a);
|
||||||
results.appendChild(li);
|
results.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user