diff --git a/layouts/shortcodes/staticsearch.html b/layouts/shortcodes/staticsearch.html
index eb926e55b47..cf25d3fd5bf 100644
--- a/layouts/shortcodes/staticsearch.html
+++ b/layouts/shortcodes/staticsearch.html
@@ -1,6 +1,5 @@
-{{/* {{ partial "_shared/banner.html" . }} */}}
-
Loading search data...
-
-
-
-
\ No newline at end of file
+
+
+
+
+
diff --git a/static/js/search.js b/static/js/search.js
index 5d2fd6c60b2..e0ffff734dc 100644
--- a/static/js/search.js
+++ b/static/js/search.js
@@ -7,12 +7,22 @@ var searchFn = function () {
"of", "at", "by", "for", "with", "to", "then", "no", "not",
"so", "too", "can", "and", "but"];
var normalizer = document.createElement("textarea");
+ // Content normalize: "pre-registration" → " pre registration preregistration "
var normalize = function (input) {
normalizer.innerHTML = input;
var inputDecoded = normalizer.value;
- return " " + inputDecoded.trim().toLowerCase().replace(/[^0-9a-z ]/gi, " ").replace(/\s+/g, " ") + " ";
+ var text = inputDecoded.trim().toLowerCase();
+ var withSpaces = text.replace(/-/g, " ");
+ var joined = (text.match(/\w+-[\w-]+/g) || []).map(function (w) { return w.replace(/-/g, ""); }).join(" ");
+ return " " + (withSpaces + " " + joined).replace(/[^0-9a-z ]/gi, " ").replace(/\s+/g, " ") + " ";
}
- var limit = 30;
+ // Query normalize: strip hyphens so "pre-registration" → " preregistration "
+ var normalizeQuery = function (input) {
+ normalizer.innerHTML = input;
+ var inputDecoded = normalizer.value;
+ return " " + inputDecoded.trim().toLowerCase().replace(/-/g, "").replace(/[^0-9a-z ]/gi, " ").replace(/\s+/g, " ") + " ";
+ }
+ var limit = 500;
var minChars = 2;
var searching = false;
var render = function (results) {
@@ -41,10 +51,22 @@ var searchFn = function () {
});
return weightResult;
};
- var search = function (terms) {
+ var search = function (terms, requiredWords) {
var results = [];
searchHost.index.forEach(function (item) {
if (item.tags) {
+ // AND logic: all query words must appear somewhere in the item
+ var allText = item.title + item.subtitle + item.description + item.content;
+ item.tags.forEach(function (tag) { allText += tag; });
+ var allMatch = true;
+ for (var w = 0; w < requiredWords.length; w++) {
+ if (!~allText.indexOf(requiredWords[w])) {
+ allMatch = false;
+ break;
+ }
+ }
+ if (!allMatch) return;
+
var weight_1 = 0;
terms.forEach(function (term) {
if (item.title.startsWith(term.term)) {
@@ -82,7 +104,7 @@ var searchFn = function () {
if (searching) {
return;
}
- var term = normalize($("#searchBox").val()).trim();
+ var term = normalizeQuery($("#searchBox").val()).trim();
if (term === lastTerm) {
return;
}
@@ -107,14 +129,23 @@ var searchFn = function () {
}
var newTerm = str.trim();
if (newTerm.length >= minChars && stopwords.indexOf(newTerm) < 0) {
+ var isPrefix = (j === terms.length - 1);
termsTree.push({
weight: weight,
- term: " " + str.trim() + " "
+ term: " " + str.trim() + (isPrefix ? "" : " ")
});
}
}
}
- search(termsTree);
+ // Build required words for AND logic (each query word must appear)
+ var requiredWords = [];
+ for (var r = 0; r < terms.length; r++) {
+ if (terms[r].length >= minChars && stopwords.indexOf(terms[r]) < 0) {
+ var isLast = (r === terms.length - 1);
+ requiredWords.push(" " + terms[r] + (isLast ? "" : " "));
+ }
+ }
+ search(termsTree, requiredWords);
searching = false;
var endSearch = new Date();
$("#results").append("Search took " + (endSearch - startSearch) + "ms.
");
diff --git a/themes/academic/assets/js/academic.js b/themes/academic/assets/js/academic.js
index 0ed7574d477..b87b2d89603 100644
--- a/themes/academic/assets/js/academic.js
+++ b/themes/academic/assets/js/academic.js
@@ -648,6 +648,9 @@
}
$container.imagesLoaded(function () {
+ let projectFilter = $section.find('.default-project-filter').text();
+ let projectSearchTerms = null;
+
// Initialize Isotope after all images have loaded.
$container.isotope({
itemSelector: '.isotope-item',
@@ -655,13 +658,48 @@
masonry: {
gutter: 20
},
- filter: $section.find('.default-project-filter').text()
+ filter: function () {
+ let $this = $(this);
+ let filterMatch = projectFilter ? $this.is(projectFilter) : true;
+ if (!filterMatch) return false;
+ if (!projectSearchTerms) return true;
+ let text = $this.text().replace(/-/g, '');
+ return projectSearchTerms.every(function (re) { return re.test(text); });
+ }
+ });
+
+ // Text search on cards.
+ let searchTimeout;
+ let $searchCount = $section.find('.search-count');
+ $section.find('.project-search').keyup(function () {
+ clearTimeout(searchTimeout);
+ let input = this;
+ searchTimeout = setTimeout(function () {
+ let val = $(input).val().trim();
+ if (val) {
+ // Split on hyphens/spaces, require all words (AND logic, prefix matching)
+ let words = val.replace(/-/g, ' ').split(/\s+/).filter(function (w) { return w.length >= 2; });
+ projectSearchTerms = words.length ? words.map(function (w) {
+ return new RegExp(w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
+ }) : null;
+ } else {
+ projectSearchTerms = null;
+ }
+ $container.isotope();
+ let count = $container.isotope('getFilteredItemElements').length;
+ let total = $container.find('.isotope-item').length;
+ if (projectSearchTerms) {
+ $searchCount.text(count + ' of ' + total + ' resources shown');
+ } else {
+ $searchCount.text('');
+ }
+ }, 200);
});
// Filter items when filter link is clicked.
$section.find('.project-filters a').click(function () {
- let selector = $(this).attr('data-filter');
- $container.isotope({filter: selector});
+ projectFilter = $(this).attr('data-filter');
+ $container.isotope();
$(this).removeClass('active').addClass('active').siblings().removeClass('active all');
return false;
});