mirror of
https://github.com/jbowdre/virtuallypotato.git
synced 2024-11-27 01:12: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();
|
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
|
&_brand
|
||||||
img
|
img
|
||||||
max-height: 2rem
|
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
|
// Magic for collapsing content, borrowed from https://www.digitalocean.com/community/tutorials/css-collapsible
|
||||||
.lbl-toggle
|
.lbl-toggle
|
||||||
|
@ -64,4 +72,59 @@ html, body
|
||||||
code
|
code
|
||||||
word-break: normal
|
word-break: normal
|
||||||
&.noClass
|
&.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]
|
[permalinks]
|
||||||
posts = ":filename"
|
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"
|
other = "and"
|
||||||
|
|
||||||
[view_source]
|
[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">
|
<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"/>
|
<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>
|
||||||
|
<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>
|
</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