* https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/ | * https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/ | ||||
* https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/ | * https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/ | ||||
*/ | */ | ||||
(function () { | |||||
;(function () { | |||||
// Retrieving the search index and stopwords from JSON. | // Retrieving the search index and stopwords from JSON. | ||||
// See https://v8.dev/blog/cost-of-javascript-2019#json | // See https://v8.dev/blog/cost-of-javascript-2019#json | ||||
let searchIndex = JSON.parse(document.getElementById('search-index').textContent) | |||||
let stopWords = JSON.parse(document.getElementById('search-stop-words').textContent) | |||||
let searchIndex = JSON.parse( | |||||
document.getElementById('search-index').textContent | |||||
) | |||||
let stopWords = JSON.parse( | |||||
document.getElementById('search-stop-words').textContent | |||||
) | |||||
// Get the DOM elements | // Get the DOM elements | ||||
let form = document.querySelector('#form-search') | let form = document.querySelector('#form-search') | ||||
let searchResultTemplate = document.querySelector('#search-result') | let searchResultTemplate = document.querySelector('#search-result') | ||||
// Make sure required content exists | // Make sure required content exists | ||||
if (!form || !input || !resultList || !searchStatus || !searchIndex || !stopWords || !searchResultTemplate) return | |||||
if ( | |||||
!form || | |||||
!input || | |||||
!resultList || | |||||
!searchStatus || | |||||
!searchIndex || | |||||
!stopWords || | |||||
!searchResultTemplate | |||||
) | |||||
return | |||||
// Create a submit handler | // Create a submit handler | ||||
form.addEventListener('submit', function (event) { | form.addEventListener('submit', function (event) { | ||||
}) | }) | ||||
// Create a reset handler | // Create a reset handler | ||||
form.addEventListener('reset', function() { | |||||
form.addEventListener('reset', function () { | |||||
search('') | search('') | ||||
searchStatus.innerHTML = '' | searchStatus.innerHTML = '' | ||||
}) | }) | ||||
/** | /** | ||||
* If there's a query string search term, search it on page load | * If there's a query string search term, search it on page load | ||||
*/ | */ | ||||
function onload () { | |||||
function onload() { | |||||
let query = new URLSearchParams(window.location.search).get('s') | let query = new URLSearchParams(window.location.search).get('s') | ||||
if (!query) return | if (!query) return | ||||
input.value = query | input.value = query | ||||
* Search for matches | * Search for matches | ||||
* @param {String} query The term to search for | * @param {String} query The term to search for | ||||
*/ | */ | ||||
function search (query) { | |||||
function search(query) { | |||||
// Create a regex for each query | // Create a regex for each query | ||||
let regMap = query.toLowerCase().split(' ').filter(function (word) { | |||||
return word.length && !stopWords.includes(word) | |||||
}).map(function (word) { | |||||
return new RegExp(word, 'i') | |||||
}) | |||||
let regMap = query | |||||
.toLowerCase() | |||||
.split(' ') | |||||
.filter(function (word) { | |||||
return word.length && !stopWords.includes(word) | |||||
}) | |||||
.map(function (word) { | |||||
return new RegExp(word, 'i') | |||||
}) | |||||
// Get and sort the results | // Get and sort the results | ||||
let results = searchIndex.reduce(function (results, article, index) { | |||||
// Setup priority count | |||||
let priority = 0 | |||||
let results = searchIndex | |||||
.reduce(function (results, article, index) { | |||||
// Setup priority count | |||||
let priority = 0 | |||||
// Assign priority | |||||
for (let reg of regMap) { | |||||
if (reg.test(article.title)) { priority += 100 } | |||||
let occurences = article.content.match(reg) | |||||
if (occurences) { priority += occurences.length } | |||||
} | |||||
// If any matches, push to results | |||||
if (priority > 0) { | |||||
results.push({ | |||||
priority: priority, | |||||
article: article | |||||
}) | |||||
} | |||||
// Assign priority | |||||
for (let reg of regMap) { | |||||
if (reg.test(article.title)) { | |||||
priority += 100 | |||||
} | |||||
let occurences = article.content.match(reg) | |||||
if (occurences) { | |||||
priority += occurences.length | |||||
} | |||||
} | |||||
return results | |||||
// If any matches, push to results | |||||
if (priority > 0) { | |||||
results.push({ | |||||
priority: priority, | |||||
article: article, | |||||
}) | |||||
} | |||||
}, []).sort(function (article1, article2) { | |||||
return article2.priority - article1.priority | |||||
}) | |||||
return results | |||||
}, []) | |||||
.sort(function (article1, article2) { | |||||
return article2.priority - article1.priority | |||||
}) | |||||
// Display the results | // Display the results | ||||
showResults(results, regMap) | showResults(results, regMap) | ||||
// Update the URL | // Update the URL | ||||
updateURL(query) | updateURL(query) | ||||
} | } | ||||
/** | /** | ||||
* @param {Array} results The results to display | * @param {Array} results The results to display | ||||
* @param {List} regMap Regular expressions for the highlights | * @param {List} regMap Regular expressions for the highlights | ||||
*/ | */ | ||||
function showResults (results, regMap) { | |||||
function showResults(results, regMap) { | |||||
let status = 'Aucune publication n’a été trouvée 😢' | let status = 'Aucune publication n’a été trouvée 😢' | ||||
let searchResults = '' | let searchResults = '' | ||||
if (results.length) { | if (results.length) { | ||||
const plural = results.length > 1 ? 's' : '' | const plural = results.length > 1 ? 's' : '' | ||||
status = `${results.length} publication${plural} trouvée${plural} 🙌` | status = `${results.length} publication${plural} trouvée${plural} 🙌` | ||||
searchResults = results.map(function (result) { | |||||
return interpolate(searchResultTemplate.innerHTML, { | |||||
url: result.article.url, | |||||
title: highlightText(result.article.title, regMap), | |||||
date: result.article.date, | |||||
content: highlightText(result.article.content, regMap), | |||||
searchResults = results | |||||
.map(function (result) { | |||||
return interpolate(searchResultTemplate.innerHTML, { | |||||
url: result.article.url, | |||||
title: highlightText(result.article.title, regMap), | |||||
date: result.article.date, | |||||
content: highlightText(result.article.content, regMap), | |||||
}) | |||||
}) | }) | ||||
}).join('') | |||||
.join('') | |||||
} | } | ||||
searchStatus.innerHTML = status | searchStatus.innerHTML = status | ||||
resultList.innerHTML = searchResults | resultList.innerHTML = searchResults | ||||
* @param {Object} params The parameters | * @param {Object} params The parameters | ||||
* @return {String} The interpolated string | * @return {String} The interpolated string | ||||
*/ | */ | ||||
function interpolate (str, params) { | |||||
let names = Object.keys(params); | |||||
let vals = Object.values(params); | |||||
return new Function(...names, `return \`${str}\``)(...vals); | |||||
function interpolate(str, params) { | |||||
let names = Object.keys(params) | |||||
let vals = Object.values(params) | |||||
return new Function(...names, `return \`${str}\``)(...vals) | |||||
} | } | ||||
/** | /** | ||||
let extracts = [] | let extracts = [] | ||||
for (let reg of regMap) { | for (let reg of regMap) { | ||||
const index = text.search(reg) | const index = text.search(reg) | ||||
if (index === -1) { continue } | |||||
if (index === -1) { | |||||
continue | |||||
} | |||||
let extract = text.substring( | let extract = text.substring( | ||||
index - extractBoundariesSize, | index - extractBoundariesSize, | ||||
index + reg.source.length + extractBoundariesSize | index + reg.source.length + extractBoundariesSize | ||||
// TODISCUSS: we replace with the source but in case there is | // TODISCUSS: we replace with the source but in case there is | ||||
// an uppercase letter it will disappear from the extract | // an uppercase letter it will disappear from the extract | ||||
// (is that confusing or closer to what is expected?) | // (is that confusing or closer to what is expected?) | ||||
extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | |||||
extract = extract.replace(reg, `<mark>${reg.source}</mark>`) | |||||
const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | ||||
const suffixEllipsis = index + extractBoundariesSize <= textLength ? '…' : '' | |||||
const suffixEllipsis = | |||||
index + extractBoundariesSize <= textLength ? '…' : '' | |||||
extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | ||||
} | } | ||||
if (!extracts.length && textLength < 200) { | if (!extracts.length && textLength < 200) { | ||||
* Update the URL with a query string for the search string | * Update the URL with a query string for the search string | ||||
* @param {String} query The search query | * @param {String} query The search query | ||||
*/ | */ | ||||
function updateURL (query) { | |||||
function updateURL(query) { | |||||
// Create the properties | // Create the properties | ||||
let state = history.state | let state = history.state | ||||
let title = document.title | let title = document.title | ||||
// Update the URL | // Update the URL | ||||
history.pushState(state, title, url) | history.pushState(state, title, url) | ||||
} | } | ||||
})() | })() | ||||
</script> | </script> |
* https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/ | * https://gomakethings.com/how-to-create-a-search-page-for-a-static-website-with-vanilla-js/ | ||||
* https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/ | * https://gomakethings.com/how-to-update-the-url-of-a-page-without-causing-a-reload-using-vanilla-javascript/ | ||||
*/ | */ | ||||
(function () { | |||||
;(function () { | |||||
// Retrieving the search index and stopwords from JSON. | // Retrieving the search index and stopwords from JSON. | ||||
// See https://v8.dev/blog/cost-of-javascript-2019#json | // See https://v8.dev/blog/cost-of-javascript-2019#json | ||||
let searchIndex = JSON.parse(document.getElementById('search-index').textContent) | |||||
let stopWords = JSON.parse(document.getElementById('search-stop-words').textContent) | |||||
let searchIndex = JSON.parse( | |||||
document.getElementById('search-index').textContent | |||||
) | |||||
let stopWords = JSON.parse( | |||||
document.getElementById('search-stop-words').textContent | |||||
) | |||||
// Get the DOM elements | // Get the DOM elements | ||||
let form = document.querySelector('#form-search') | let form = document.querySelector('#form-search') | ||||
let searchResultTemplate = document.querySelector('#search-result') | let searchResultTemplate = document.querySelector('#search-result') | ||||
// Make sure required content exists | // Make sure required content exists | ||||
if (!form || !input || !resultList || !searchStatus || !searchIndex || !stopWords || !searchResultTemplate) return | |||||
if ( | |||||
!form || | |||||
!input || | |||||
!resultList || | |||||
!searchStatus || | |||||
!searchIndex || | |||||
!stopWords || | |||||
!searchResultTemplate | |||||
) | |||||
return | |||||
// Create a submit handler | // Create a submit handler | ||||
form.addEventListener('submit', function (event) { | form.addEventListener('submit', function (event) { | ||||
}) | }) | ||||
// Create a reset handler | // Create a reset handler | ||||
form.addEventListener('reset', function() { | |||||
form.addEventListener('reset', function () { | |||||
search('') | search('') | ||||
searchStatus.innerHTML = '' | searchStatus.innerHTML = '' | ||||
}) | }) | ||||
/** | /** | ||||
* If there's a query string search term, search it on page load | * If there's a query string search term, search it on page load | ||||
*/ | */ | ||||
function onload () { | |||||
function onload() { | |||||
let query = new URLSearchParams(window.location.search).get('s') | let query = new URLSearchParams(window.location.search).get('s') | ||||
if (!query) return | if (!query) return | ||||
input.value = query | input.value = query | ||||
* Search for matches | * Search for matches | ||||
* @param {String} query The term to search for | * @param {String} query The term to search for | ||||
*/ | */ | ||||
function search (query) { | |||||
function search(query) { | |||||
// Create a regex for each query | // Create a regex for each query | ||||
let regMap = query.toLowerCase().split(' ').filter(function (word) { | |||||
return word.length && !stopWords.includes(word) | |||||
}).map(function (word) { | |||||
return new RegExp(word, 'i') | |||||
}) | |||||
let regMap = query | |||||
.toLowerCase() | |||||
.split(' ') | |||||
.filter(function (word) { | |||||
return word.length && !stopWords.includes(word) | |||||
}) | |||||
.map(function (word) { | |||||
return new RegExp(word, 'i') | |||||
}) | |||||
// Get and sort the results | // Get and sort the results | ||||
let results = searchIndex.reduce(function (results, article, index) { | |||||
// Setup priority count | |||||
let priority = 0 | |||||
let results = searchIndex | |||||
.reduce(function (results, article, index) { | |||||
// Setup priority count | |||||
let priority = 0 | |||||
// Assign priority | |||||
for (let reg of regMap) { | |||||
if (reg.test(article.title)) { priority += 100 } | |||||
let occurences = article.content.match(reg) | |||||
if (occurences) { priority += occurences.length } | |||||
} | |||||
// If any matches, push to results | |||||
if (priority > 0) { | |||||
results.push({ | |||||
priority: priority, | |||||
article: article | |||||
}) | |||||
} | |||||
// Assign priority | |||||
for (let reg of regMap) { | |||||
if (reg.test(article.title)) { | |||||
priority += 100 | |||||
} | |||||
let occurences = article.content.match(reg) | |||||
if (occurences) { | |||||
priority += occurences.length | |||||
} | |||||
} | |||||
return results | |||||
// If any matches, push to results | |||||
if (priority > 0) { | |||||
results.push({ | |||||
priority: priority, | |||||
article: article, | |||||
}) | |||||
} | |||||
}, []).sort(function (article1, article2) { | |||||
return article2.priority - article1.priority | |||||
}) | |||||
return results | |||||
}, []) | |||||
.sort(function (article1, article2) { | |||||
return article2.priority - article1.priority | |||||
}) | |||||
// Display the results | // Display the results | ||||
showResults(results, regMap) | showResults(results, regMap) | ||||
// Update the URL | // Update the URL | ||||
updateURL(query) | updateURL(query) | ||||
} | } | ||||
/** | /** | ||||
* @param {Array} results The results to display | * @param {Array} results The results to display | ||||
* @param {List} regMap Regular expressions for the highlights | * @param {List} regMap Regular expressions for the highlights | ||||
*/ | */ | ||||
function showResults (results, regMap) { | |||||
function showResults(results, regMap) { | |||||
let status = 'Aucune publication n’a été trouvée 😢' | let status = 'Aucune publication n’a été trouvée 😢' | ||||
let searchResults = '' | let searchResults = '' | ||||
if (results.length) { | if (results.length) { | ||||
const plural = results.length > 1 ? 's' : '' | const plural = results.length > 1 ? 's' : '' | ||||
status = `${results.length} publication${plural} trouvée${plural} 🙌` | status = `${results.length} publication${plural} trouvée${plural} 🙌` | ||||
searchResults = results.map(function (result) { | |||||
return interpolate(searchResultTemplate.innerHTML, { | |||||
url: result.article.url, | |||||
title: highlightText(result.article.title, regMap), | |||||
date: result.article.date, | |||||
content: highlightText(result.article.content, regMap), | |||||
searchResults = results | |||||
.map(function (result) { | |||||
return interpolate(searchResultTemplate.innerHTML, { | |||||
url: result.article.url, | |||||
title: highlightText(result.article.title, regMap), | |||||
date: result.article.date, | |||||
content: highlightText(result.article.content, regMap), | |||||
}) | |||||
}) | }) | ||||
}).join('') | |||||
.join('') | |||||
} | } | ||||
searchStatus.innerHTML = status | searchStatus.innerHTML = status | ||||
resultList.innerHTML = searchResults | resultList.innerHTML = searchResults | ||||
* @param {Object} params The parameters | * @param {Object} params The parameters | ||||
* @return {String} The interpolated string | * @return {String} The interpolated string | ||||
*/ | */ | ||||
function interpolate (str, params) { | |||||
let names = Object.keys(params); | |||||
let vals = Object.values(params); | |||||
return new Function(...names, `return \`${str}\``)(...vals); | |||||
function interpolate(str, params) { | |||||
let names = Object.keys(params) | |||||
let vals = Object.values(params) | |||||
return new Function(...names, `return \`${str}\``)(...vals) | |||||
} | } | ||||
/** | /** | ||||
let extracts = [] | let extracts = [] | ||||
for (let reg of regMap) { | for (let reg of regMap) { | ||||
const index = text.search(reg) | const index = text.search(reg) | ||||
if (index === -1) { continue } | |||||
if (index === -1) { | |||||
continue | |||||
} | |||||
let extract = text.substring( | let extract = text.substring( | ||||
index - extractBoundariesSize, | index - extractBoundariesSize, | ||||
index + reg.source.length + extractBoundariesSize | index + reg.source.length + extractBoundariesSize | ||||
// TODISCUSS: we replace with the source but in case there is | // TODISCUSS: we replace with the source but in case there is | ||||
// an uppercase letter it will disappear from the extract | // an uppercase letter it will disappear from the extract | ||||
// (is that confusing or closer to what is expected?) | // (is that confusing or closer to what is expected?) | ||||
extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | |||||
extract = extract.replace(reg, `<mark>${reg.source}</mark>`) | |||||
const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | ||||
const suffixEllipsis = index + extractBoundariesSize <= textLength ? '…' : '' | |||||
const suffixEllipsis = | |||||
index + extractBoundariesSize <= textLength ? '…' : '' | |||||
extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | ||||
} | } | ||||
if (!extracts.length && textLength < 200) { | if (!extracts.length && textLength < 200) { | ||||
* Update the URL with a query string for the search string | * Update the URL with a query string for the search string | ||||
* @param {String} query The search query | * @param {String} query The search query | ||||
*/ | */ | ||||
function updateURL (query) { | |||||
function updateURL(query) { | |||||
// Create the properties | // Create the properties | ||||
let state = history.state | let state = history.state | ||||
let title = document.title | let title = document.title | ||||
// Update the URL | // Update the URL | ||||
history.pushState(state, title, url) | history.pushState(state, title, url) | ||||
} | } | ||||
})() | })() | ||||
</script> | </script> |