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

General: Create Site Search #302

Merged
merged 7 commits into from Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions _includes/_header.htm
Expand Up @@ -48,6 +48,7 @@ <h6 class="dropdown-header text-center">Publications</h6>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header text-center">GRC Info</h6>
<a class="dropdown-item" href="/wiki/">Gridcoin Wiki</a>
<a class="dropdown-item" href="/search.htm">Search this Site</a>
<a class="dropdown-item" href="https://github.com/gridcoin-community/Gridcoin-Marketing">Gridcoin Marketing and Media Files</a>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header text-center">Development</h6>
Expand Down Expand Up @@ -128,6 +129,11 @@ <h6 class="dropdown-header text-center">Exchange Stats</h6>
<span class="oi oi-data-transfer-download"></span>
</a>
</div>
<div class="nav-item" data-toggle="collapse" data-target=".navbar-collapse.show" >
<a href="/search.htm"class="nav-link btn grcbtn" role="tooltip" title="Search this site" aria-label="Search this site" data-toggle="tooltip">
<span class="oi oi-magnifying-glass"></span>
</a>
</div>
</div>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions _layouts/search-layout.htm
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html class="h-100" lang="en">
{% include _head.htm %}
<body>
{% include _cookie_banner.htm %}
{% include _header.htm %}
<div class="min-h-80 container-fluid search-container" id="main" role="main">{{ content | strip_newlines }}</div>
{% include _footer.htm %}

<!-- Core JS Files -->
<script src="/assets/js/core/jquery-3.6.0.min.js" type="text/javascript"></script>
<script src="/assets/js/core/bootstrap-4.6.0.bundle.min.js" type="text/javascript"></script>
<script src="/assets/js/lunr.min.js" type="text/javascript"></script>
<script src="/assets/js/search.js" type="text/javascript"></script>
</body>
</html>
8 changes: 8 additions & 0 deletions assets/css/gridcoin.css
Expand Up @@ -436,3 +436,11 @@ dfn{
bottom: 0px;
z-index: 1040;
}
/* search.htm */
.search-container{
padding-top: 130px;
}
.search-container li{
list-style-type: none;
}
/* end search.html */
6 changes: 6 additions & 0 deletions assets/js/lunr.min.js

Large diffs are not rendered by default.

155 changes: 155 additions & 0 deletions assets/js/search.js
@@ -0,0 +1,155 @@
---
layout:
---
(function () {
const pageData = {
{%- for page in site.html_pages -%}
{%- if page.content != "" and page.layout != ""-%}
"{{ page.url | slugify }}": {
"title": "{{ page.title | xml_escape }}",
"content": {{ page.content | strip_html | markdownify | strip_html | normalize_whitespace | jsonify }},
"url": "{{ page.url | xml_escape }}",
}
{% unless forloop.last %},{% endunless %}
{%- endif -%}
{%- endfor -%}
};
{% comment %} strip HTML the first time so it removes from .htm page and then do it again to remove from markdown pages after rendering. Markdown rendering will escape HTML so has to be this order{% endcomment %}


function determineMatchesToShow(matches) {
//find how close the matches are and display the first bunch
//returns the index of the last match

if (!matches["content"]) {
return null; //doesn't make sense to look if it matched only in the tile
}

const positions = matches["content"].position;

var diff = 0;

for (var i = 0; i < (positions.length - 2) && diff < 200; i++) {
// compare start of the next to end of the current
diff = positions[i + 1][0] - (positions[i][0] + positions[i][1]);
}

//increment in for loop is done before check, so subtract 1
return Math.max(0, i - 1);
}

function closestWordIndex(string, start) {
for (var i = start; i > 0 && string[i] != ' '; i--) {
//seeks backward until space or start of the string
}

return i;
}

function displaySearchResults(results, store) {
var searchResults = document.getElementById('search-results');

if (results.length) { // display message about no results if there are none

for (var i = 0; i < results.length; i++) {
var result = results[i];
var item = store[result.ref];


var li = document.createElement('li');

var h3 = document.createElement('h3');
var a = document.createElement('a');
a.href = item.url;
a.textContent = item.title;

h3.appendChild(a);
li.appendChild(h3);

var p = document.createElement('p');

var matchData = Object.values(result.matchData.metadata)[0];
//workaround for weird bug where some search terms don't get the correct label

var matchCount = determineMatchesToShow(matchData);

if (matchCount !== null) {
var positions = matchData["content"].position;

const lookBackMin = 10;
const firstPos = closestWordIndex(item.content, positions[0][0] - lookBackMin);

var lookAhead = 10;

if (matchCount === 0) {
//give a little context in case there's only one match
lookAhead = 75; //roughly length of average sentence
}
const lastPos = closestWordIndex(item.content, positions[matchCount][0] + positions[matchCount][1] + lookAhead);

if (firstPos !== 0) {
p.textContent += "...";
}
p.textContent += item.content.substring(firstPos, lastPos);

if (lastPos != item.content.length - 1) {
p.textContent += "...";
}

} else {
//if only title or other matches, just put first 150 chars
p.textContent = item.content.substring(0, 150) + "...";
}

li.appendChild(p);

searchResults.appendChild(li);
}

} else {
var li = document.createElement("li");
li.innerText = "No Results Found";

searchResults.appendChild(li);
}
}

//reference: https://learn.cloudcannon.com/jekyll/jekyll-search-using-lunr-js/
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split('&');

for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');

if (pair[0] === variable) {
return decodeURIComponent(pair[1].replace(/\+/g, '%20'));
}
}
}

var searchTerm = getQueryVariable('query');

if (searchTerm) {
document.getElementById('search-box')
.setAttribute("value", searchTerm);

var idx = lunr(function () {
this.field('id');
this.field('title', {boost: 10});
this.field('content');

this.metadataWhitelist = ['position'];

for (key in pageData) {
this.add({
'id': key,
'title': pageData[key].title,
'content': pageData[key].content
})
};
});
var results = idx.search(searchTerm);
displaySearchResults(results, pageData);
}
})();
24 changes: 24 additions & 0 deletions search.htm
@@ -0,0 +1,24 @@
---
title: Search the Site
layout: search-layout
description: Let you look for information on this site
---
<div class="row justify-content-center">
<noscript><div class="col-12 mb-5"><b class="h1">This requires Javascript to work since searching is done client side. Either enable Javascript or try using a search engine and prefix it with <code>site:gridcoin.us</code> instead</b></div></noscript>
<div class="col-md-8 offset-md-3 col-auto">
<label class="sr-only" for="search-box">Search</label>
<form action="/search.htm" class="form-inline" method="get">
<div class="input-group col-md-8">
<div class="input-group-prepend">
<div class="input-group-text"><span class="oi oi-magnifying-glass"></span></div>
</div>
<input type="text" placeholder="your search" id="search-box" name="query" class="form-control text-dark">
<input type="submit" value="search" class="form-control btn btn-primary col-md-3">
</div>
</form>
</div>
<div class="w-100"></div>
<div class="col-12 col-md-6 mt-3">
<ul id="search-results"></ul>
</div>
</div>