mirror of
https://github.com/jbowdre/virtuallypotato.git
synced 2024-12-24 03:42:19 +00:00
implement search capability from https://github.com/onweru/compose
This commit is contained in:
parent
0c1d9100f5
commit
9ed2c8d9c9
12 changed files with 444 additions and 3 deletions
|
@ -10,4 +10,76 @@ Array.from(myLabels).forEach(label => {
|
|||
label.click();
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Search helpers from https://github.com/onweru/compose
|
||||
const rootURL = window.location.protocol + "//" + window.location.host;
|
||||
const searchFieldClass = '.search_field';
|
||||
const searchClass = '.search';
|
||||
const quickLinks = '{{ T "quick_links" }}';
|
||||
const searchResultsLabel = '{{ T "search_results_label" }}';
|
||||
const shortSearchQuery = '{{ T "short_search_query" }}'
|
||||
const typeToSearch = '{{ T "type_to_search" }}';
|
||||
const noMatchesFound = '{{ T "no_matches" }}';
|
||||
|
||||
function createEl(element = 'div') {
|
||||
return document.createElement(element);
|
||||
}
|
||||
|
||||
function emptyEl(el) {
|
||||
while(el.firstChild)
|
||||
el.removeChild(el.firstChild);
|
||||
}
|
||||
|
||||
function wrapText(text, context, wrapper = 'mark') {
|
||||
let open = `<${wrapper}>`;
|
||||
let close = `</${wrapper}>`;
|
||||
let escapedOpen = `%3C${wrapper}%3E`;
|
||||
let escapedClose = `%3C/${wrapper}%3E`;
|
||||
function wrap(context) {
|
||||
let c = context.innerHTML;
|
||||
let pattern = new RegExp(text, "gi");
|
||||
let matches = text.length ? c.match(pattern) : null;
|
||||
|
||||
if(matches) {
|
||||
matches.forEach(function(matchStr){
|
||||
c = c.replaceAll(matchStr, `${open}${matchStr}${close}`);
|
||||
context.innerHTML = c;
|
||||
});
|
||||
|
||||
const images = elems('img', context);
|
||||
|
||||
if(images) {
|
||||
images.forEach(image => {
|
||||
image.src = image.src.replaceAll(open, '').replaceAll(close, '').replaceAll(escapedOpen, '').replaceAll(escapedClose, '');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contents = ["h1", "h2", "h3", "h4", "h5", "h6", "p", "code", "td"];
|
||||
|
||||
contents.forEach(function(c){
|
||||
const cs = elems(c, context);
|
||||
if(cs.length) {
|
||||
cs.forEach(function(cx, index){
|
||||
if(cx.children.length >= 1) {
|
||||
Array.from(cx.children).forEach(function(child){
|
||||
wrap(child);
|
||||
})
|
||||
wrap(cx);
|
||||
} else {
|
||||
wrap(cx);
|
||||
}
|
||||
// sanitize urls and ids
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const hyperLinks = elems('a');
|
||||
if(hyperLinks) {
|
||||
hyperLinks.forEach(function(link){
|
||||
link.href = link.href.replaceAll(encodeURI(open), "").replaceAll(encodeURI(close), "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,14 @@ html, body
|
|||
&_brand
|
||||
img
|
||||
max-height: 2rem
|
||||
// search box
|
||||
&-item
|
||||
display: grid
|
||||
align-items: center
|
||||
.search
|
||||
@media screen and (min-width: 992px)
|
||||
margin-right: 1.5rem
|
||||
|
||||
|
||||
// Magic for collapsing content, borrowed from https://www.digitalocean.com/community/tutorials/css-collapsible
|
||||
.lbl-toggle
|
||||
|
@ -64,4 +72,59 @@ html, body
|
|||
code
|
||||
word-break: normal
|
||||
&.noClass
|
||||
line-break: normal
|
||||
line-break: normal
|
||||
|
||||
|
||||
// search from https://github.com/onweru/compose
|
||||
.search
|
||||
flex: 1
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
position: relative
|
||||
&_field
|
||||
padding: 0.5rem 1.5rem 0.5rem 2.5rem
|
||||
border-radius: 1.5rem
|
||||
width: 13.5rem
|
||||
outline: none
|
||||
border: none
|
||||
background: transparent
|
||||
color: var(--text)
|
||||
box-shadow: 0 1rem 4rem rgba(0,0,0,0.17)
|
||||
font-size: 1rem
|
||||
&_label
|
||||
width: 1rem
|
||||
height: 1rem
|
||||
position: absolute
|
||||
left: 0.33rem
|
||||
top: 0.25rem
|
||||
opacity: 0.33
|
||||
svg
|
||||
width: 100%
|
||||
height: 100%
|
||||
fill: var(--text)
|
||||
&_result
|
||||
padding: 0.5rem 1rem
|
||||
&:not(.passive):hover
|
||||
background-color: var(--theme)
|
||||
color: var(--light)
|
||||
&.passive
|
||||
display: grid
|
||||
&s
|
||||
width: 13.5rem
|
||||
background-color: var(--bg)
|
||||
border-radius: 0 0 0.25rem 0.25rem
|
||||
box-shadow: 0 1rem 4rem rgba(0,0,0,0.17)
|
||||
position: absolute
|
||||
top: 125%
|
||||
display: grid
|
||||
overflow: hidden
|
||||
z-index: 5
|
||||
&:empty
|
||||
display: none
|
||||
&_title
|
||||
padding: 0.5rem 1rem 0.5rem 1rem
|
||||
background: var(--theme)
|
||||
color: var(--light)
|
||||
font-size: 0.9rem
|
||||
opacity: 0.87
|
||||
text-transform: uppercase
|
||||
|
|
|
@ -10,3 +10,6 @@ DefaultContentLanguage = "en"
|
|||
|
||||
[permalinks]
|
||||
posts = ":filename"
|
||||
|
||||
[outputs]
|
||||
home = ["HTML", "RSS","JSON"]
|
5
content/search.md
Normal file
5
content/search.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
+++
|
||||
title = "Search"
|
||||
searchPage = true
|
||||
type = "search"
|
||||
+++
|
20
i18n/en.toml
20
i18n/en.toml
|
@ -14,4 +14,22 @@ other = "Powered by"
|
|||
other = "and"
|
||||
|
||||
[view_source]
|
||||
other = "View source"
|
||||
other = "View source"
|
||||
|
||||
[no_matches]
|
||||
other = "No matches found"
|
||||
|
||||
[quick_links]
|
||||
other = "Quick links"
|
||||
|
||||
[search_field_placeholder]
|
||||
other = "Search"
|
||||
|
||||
[search_results_label]
|
||||
other = "Search Results"
|
||||
|
||||
[short_search_query]
|
||||
other = "Query is too short"
|
||||
|
||||
[type_to_search]
|
||||
other = "Type to search"
|
7
layouts/_default/index.json
Normal file
7
layouts/_default/index.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{{- $.Scratch.Add "index" slice -}}
|
||||
{{- range .Site.Pages -}}
|
||||
{{- if ne .Type "search" -}}
|
||||
{{- $.Scratch.Add "index" (dict "title" .Title "body" .Plain "link" .Permalink) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- jsonify (uniq ($.Scratch.Get "index")) -}}
|
|
@ -61,4 +61,7 @@
|
|||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="edit">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/><path d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"/>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" id="search">
|
||||
<path d="M508.874 478.708L360.142 329.976c28.21-34.827 45.191-79.103 45.191-127.309C405.333 90.917 314.416 0 202.666 0S0 90.917 0 202.667s90.917 202.667 202.667 202.667c48.206 0 92.482-16.982 127.309-45.191l148.732 148.732c4.167 4.165 10.919 4.165 15.086 0l15.081-15.082c4.165-4.166 4.165-10.92-.001-15.085zM202.667 362.667c-88.229 0-160-71.771-160-160s71.771-160 160-160 160 71.771 160 160-71.771 160-160 160z"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 10 KiB |
39
layouts/partials/nav.html
Normal file
39
layouts/partials/nav.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{{- $menu := .menu }}
|
||||
{{ $menuData := .context.Site.Data.menu }}
|
||||
{{- $link := .context.Permalink }}
|
||||
{{- $url := "" }}
|
||||
{{- $name := "" }}
|
||||
{{- $forwardSlash := "/" }}
|
||||
{{- $children := false }}
|
||||
{{- range $menu }}
|
||||
{{- if eq $menu $menuData }}
|
||||
{{- $children = .submenu }}
|
||||
{{- $name = .name }}
|
||||
{{- $url = absURL .link }}
|
||||
{{- else }}
|
||||
{{- $children = .Children }}
|
||||
{{- $name = .Name }}
|
||||
{{- $url = absLangURL .URL }}
|
||||
{{- end }}
|
||||
<div class="nav_parent{{ if (and (not .Children) (eq (trim $url $forwardSlash) (trim $link $forwardSlash))) }} nav_active{{ end}}">
|
||||
<a href="{{ $url }}" class="nav_item" title="{{ $name }}">{{ $name }} {{ with $children }}<img src='{{ absURL "icons/caret-icon.svg" }}' alt="icon" class="nav_icon">{{ end }}</a>
|
||||
{{- with $children }}
|
||||
<div class="nav_sub">
|
||||
<span class="nav_child"></span>
|
||||
{{- range . }}
|
||||
{{- if eq $menu $menuData }}
|
||||
{{- $name = .name }}
|
||||
{{- $url = absURL .link }}
|
||||
{{- else }}
|
||||
{{- $name = .Name }}
|
||||
{{- $url = absLangURL .URL }}
|
||||
{{- end }}
|
||||
<a href="{{ $url }}" class="nav_child nav_item" title="{{ $name }}">{{ $name }}</a>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
<li class="nav-item">
|
||||
{{- partial "search" . }}
|
||||
</li>
|
12
layouts/partials/search.html
Normal file
12
layouts/partials/search.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!-- search from https://github.com/onweru/compose -->
|
||||
<div class="search">
|
||||
<label for="find" class="search_label">
|
||||
<svg class="icon icon_search">
|
||||
<use xlink:href="#search"></use>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="search" class="search_field" placeholder='{{ T "search_field_placeholder" }}' id="find" autocomplete="off">
|
||||
<div class="search_results results"></div>
|
||||
</div>
|
||||
<script src="{{ "/js/fuse.js" | relURL }}"></script>
|
||||
<script src="{{ "/js/search.js" | relURL }}"></script>
|
5
layouts/search/single.html
Normal file
5
layouts/search/single.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{{- define "main" }}
|
||||
<main id="results">
|
||||
<div class="results" id="searchpage"></div>
|
||||
</main>
|
||||
{{- end }}
|
9
static/js/fuse.js
Normal file
9
static/js/fuse.js
Normal file
File diff suppressed because one or more lines are too long
205
static/js/search.js
Normal file
205
static/js/search.js
Normal file
|
@ -0,0 +1,205 @@
|
|||
// search from https://github.com/onweru/compose
|
||||
|
||||
function initializeSearch(index) {
|
||||
const searchKeys = ['title', 'link', 'body', 'id'];
|
||||
|
||||
const searchPageElement = elem('#searchpage');
|
||||
|
||||
const searchOptions = {
|
||||
ignoreLocation: true,
|
||||
findAllMatches: true,
|
||||
includeScore: true,
|
||||
shouldSort: true,
|
||||
keys: searchKeys,
|
||||
threshold: 0.0
|
||||
};
|
||||
|
||||
index = new Fuse(index, searchOptions);
|
||||
|
||||
function minQueryLen(query) {
|
||||
query = query.trim();
|
||||
const queryIsFloat = parseFloat(query);
|
||||
const minimumQueryLength = queryIsFloat ? 1 : 2;
|
||||
return minimumQueryLength;
|
||||
}
|
||||
|
||||
function searchResults(results=[], query="", passive = false) {
|
||||
let resultsFragment = new DocumentFragment();
|
||||
let showResults = elem('.search_results');
|
||||
if(passive) {
|
||||
showResults = searchPageElement;
|
||||
}
|
||||
emptyEl(showResults);
|
||||
|
||||
const queryLen = query.length;
|
||||
const requiredQueryLen = minQueryLen(query);
|
||||
|
||||
if(results.length && queryLen >= requiredQueryLen) {
|
||||
let resultsTitle = createEl('h3');
|
||||
resultsTitle.className = 'search_title';
|
||||
resultsTitle.innerText = quickLinks;
|
||||
if(passive) {
|
||||
resultsTitle.innerText = searchResultsLabel;
|
||||
}
|
||||
resultsFragment.appendChild(resultsTitle);
|
||||
if(!searchPageElement) {
|
||||
results = results.slice(0,8);
|
||||
} else {
|
||||
results = results.slice(0,12);
|
||||
}
|
||||
results.forEach(function(result){
|
||||
let item = createEl('a');
|
||||
item.href = `${result.link}?query=${query}`;
|
||||
item.className = 'search_result';
|
||||
item.style.order = result.score;
|
||||
if(passive) {
|
||||
pushClass(item, 'passive');
|
||||
let itemTitle = createEl('h3');
|
||||
itemTitle.textContent = result.title;
|
||||
item.appendChild(itemTitle);
|
||||
|
||||
let itemDescription = createEl('p');
|
||||
// position of first search term instance
|
||||
let queryInstance = result.body.indexOf(query);
|
||||
itemDescription.textContent = `... ${result.body.substring(queryInstance, queryInstance + 200)} ...`;
|
||||
item.appendChild(itemDescription);
|
||||
} else {
|
||||
item.textContent = result.title;
|
||||
}
|
||||
resultsFragment.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
if(queryLen >= requiredQueryLen) {
|
||||
if (!results.length) {
|
||||
showResults.innerHTML = `<span class="search_result">${noMatchesFound}</span>`;
|
||||
}
|
||||
} else {
|
||||
showResults.innerHTML = `<label for="find" class="search_result">${ queryLen > 1 ? shortSearchQuery : typeToSearch }</label>`
|
||||
}
|
||||
|
||||
showResults.appendChild(resultsFragment);
|
||||
}
|
||||
|
||||
function search(searchTerm, passive = false) {
|
||||
if(searchTerm.length) {
|
||||
let rawResults = index.search(searchTerm);
|
||||
rawResults = rawResults.map(function(result){
|
||||
const score = result.score;
|
||||
const resultItem = result.item;
|
||||
resultItem.score = (parseFloat(score) * 50).toFixed(0);
|
||||
return resultItem;
|
||||
});
|
||||
|
||||
passive ? searchResults(rawResults, searchTerm, true) : searchResults(rawResults, searchTerm);
|
||||
|
||||
} else {
|
||||
passive ? searchResults([], "", true) : searchResults();
|
||||
}
|
||||
}
|
||||
|
||||
function liveSearch() {
|
||||
const searchField = elem(searchFieldClass);
|
||||
|
||||
if (searchField) {
|
||||
searchField.addEventListener('input', function() {
|
||||
const searchTerm = searchField.value.trim().toLowerCase();
|
||||
search(searchTerm);
|
||||
});
|
||||
|
||||
if(!searchPageElement) {
|
||||
searchField.addEventListener('search', function(){
|
||||
const searchTerm = searchField.value.trim().toLowerCase();
|
||||
if(searchTerm.length) {
|
||||
window.location.href = new URL(`search/?query=${searchTerm}`, rootURL).href;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findQuery(query = 'query') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if(urlParams.has(query)){
|
||||
let c = urlParams.get(query);
|
||||
return c;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function passiveSearch() {
|
||||
if(searchPageElement) {
|
||||
const searchTerm = findQuery();
|
||||
search(searchTerm, true);
|
||||
|
||||
// search actively after search page has loaded
|
||||
const searchField = elem(searchFieldClass);
|
||||
|
||||
if(searchField) {
|
||||
searchField.addEventListener('input', function() {
|
||||
const searchTerm = searchField.value.trim().toLowerCase();
|
||||
search(searchTerm, true);
|
||||
wrapText(searchTerm, main);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasSearchResults() {
|
||||
const searchResults = elem('.results');
|
||||
if(searchResults) {
|
||||
const body = searchResults.innerHTML.length;
|
||||
return [searchResults, body];
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function clearSearchResults() {
|
||||
let searchResults = hasSearchResults();
|
||||
if(searchResults) {
|
||||
searchResults = searchResults[0];
|
||||
searchResults.innerHTML = "";
|
||||
// clear search field
|
||||
const searchField = elem(searchFieldClass);
|
||||
searchField.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function onEscape(fn){
|
||||
window.addEventListener('keydown', function(event){
|
||||
if(event.code === "Escape") {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let main = elem('main');
|
||||
if(!main) {
|
||||
main = elem('.main');
|
||||
}
|
||||
|
||||
searchPageElement ? false : liveSearch();
|
||||
passiveSearch();
|
||||
|
||||
wrapText(findQuery(), main);
|
||||
|
||||
onEscape(clearSearchResults);
|
||||
|
||||
window.addEventListener('click', function(event){
|
||||
const target = event.target;
|
||||
const isSearch = target.closest(searchClass) || target.matches(searchClass);
|
||||
if(!isSearch && !searchPageElement) {
|
||||
clearSearchResults();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
fetch(new URL("index.json", rootURL).href)
|
||||
.then(response => response.json())
|
||||
.then(function(data) {
|
||||
data = data.length ? data : [];
|
||||
initializeSearch(data);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
});
|
Loading…
Reference in a new issue