diff --git a/.eslintrc b/.eslintrc index 5c11324..1bcf8e0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,6 +7,6 @@ "globals": { "mermaid": true, "$": true, - "process": true + "URLSearchParams": true } } diff --git a/content/search.md b/content/search.md new file mode 100644 index 0000000..f290ee3 --- /dev/null +++ b/content/search.md @@ -0,0 +1,7 @@ +--- +title: Search +layout: search +outputs: [json, html] +image: unsplash:mdS647i6-ZM +hidden_from_navbar: true +--- diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index fcdedcf..20c9506 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -41,6 +41,8 @@ {{ partial "script_async.html" "js/app.js" }} + {{ block "scripts_extra" . }}{{ end }} + {{ if not .Site.IsServer }} {{ end }} diff --git a/layouts/_default/search.html b/layouts/_default/search.html new file mode 100644 index 0000000..cb9d021 --- /dev/null +++ b/layouts/_default/search.html @@ -0,0 +1,29 @@ +{{ define "main" }} + {{ $pages := where .Site.RegularPages "Section" "!=" ""}} + +
+
+ {{ partial "content.html" . }} +
+
+
+
+ +
+ +

+ +
+ {{ range $pages }} + {{ partial "list_item.html" . }} + {{ end }} +
+ +
+
+
+{{ end }} + +{{ define "scripts_extra" }} + {{ partial "script_async.html" "js/search.js" }} +{{ end }} diff --git a/layouts/_default/search.json b/layouts/_default/search.json new file mode 100644 index 0000000..53a1ef1 --- /dev/null +++ b/layouts/_default/search.json @@ -0,0 +1,8 @@ +{{- $result := slice -}} + +{{- range (where .Site.RegularPages "Section" "!=" "") -}} + {{- $data := dict "title" .Title "content" .Plain "id" (.File.UniqueID) -}} + {{- $result = $result | append $data -}} +{{- end -}} + +{{ jsonify $result }} diff --git a/layouts/index.html b/layouts/index.html index 388d2f2..6598a7a 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -5,6 +5,12 @@

{{ .Site.Params.author_name }}

{{ .Content }}

+ +
+
+ +
+
{{ end }} diff --git a/layouts/partials/list_item.html b/layouts/partials/list_item.html index f33364c..bad6c52 100644 --- a/layouts/partials/list_item.html +++ b/layouts/partials/list_item.html @@ -1,4 +1,4 @@ -
+
{{ if .Params.image }} diff --git a/package-lock.json b/package-lock.json index 2e3fb35..9716bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "elevator.js": "1.0.1", "jquery": "3.6.0", "lg-thumbnail": "1.2.1", - "lightgallery": "1.10.0" + "lightgallery": "1.10.0", + "lunr": "2.3.9" }, "devDependencies": { "broken-link-checker-local": "0.2.1", @@ -4564,6 +4565,11 @@ "yallist": "^2.1.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + }, "node_modules/map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -11480,6 +11486,11 @@ "yallist": "^2.1.2" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", diff --git a/package.json b/package.json index d3633b5..c333d26 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "elevator.js": "1.0.1", "jquery": "3.6.0", "lg-thumbnail": "1.2.1", - "lightgallery": "1.10.0" + "lightgallery": "1.10.0", + "lunr": "2.3.9" } } diff --git a/scripts/build.sh b/scripts/build.sh index efa8603..2881f92 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,7 +8,7 @@ rm -rf $OUTPUT_DIR rm -rf $STATIC_BUILD rm -rf $BASEDIR/resources -mkdir -p $STATIC_BUILD/js $STATIC_BUILD/css $STATIC_BUILD/audio +mkdir -p $STATIC_BUILD/css $STATIC_BUILD/audio cp -r $BASEDIR/node_modules/lightgallery/dist/fonts $STATIC_BUILD cp -r $STATIC_SRC/img $STATIC_BUILD/img @@ -17,7 +17,7 @@ cp -r $BASEDIR/node_modules/@fortawesome/fontawesome-free/css/all.min.css $STATI cp $BASEDIR/node_modules/lightgallery/dist/css/lightgallery.min.css $STATIC_BUILD/css/lightgallery.css cp -r $BASEDIR/node_modules/@fortawesome/fontawesome-free/webfonts $STATIC_BUILD -cp $STATIC_SRC/js/app.js $STATIC_BUILD/js/app.js +cp -r $STATIC_SRC/js/ $STATIC_BUILD/js/ cp $BASEDIR/node_modules/jquery/dist/jquery.min.js $STATIC_BUILD/js/jquery.min.js cp $BASEDIR/node_modules/lightgallery/dist/js/lightgallery-all.min.js $STATIC_BUILD/js/lightgallery.min.js cp $BASEDIR/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js $STATIC_BUILD/js/bootstrap.min.js diff --git a/static/src/js/search.js b/static/src/js/search.js new file mode 100644 index 0000000..bac97ac --- /dev/null +++ b/static/src/js/search.js @@ -0,0 +1,110 @@ +const lunr = require('lunr'); + +const MIN_SEARCH_LENGTH = 3; + +const searchInput = $('#search-input'); +const searchResultsMessage = $('#search-results-message'); +const searchResultsCount = $('#search-results-count'); +const searchResultsList = $('.search-results'); +const searchResults = searchResultsList.children(); + +let searchIndex; + +function showMessage(message) { + if (message) { + searchResults.hide(); + searchResultsCount.hide(); + searchResultsMessage.show(); + searchResultsMessage.text(message); + } else { + searchResultsMessage.hide(); + } +} + +function getInitialSearchTerm() { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('q') || ''; +} + +function onSearch(term) { + if (term.length < MIN_SEARCH_LENGTH) { + searchResultsCount.hide(); + showMessage( + `Enter a search term (of at least ${MIN_SEARCH_LENGTH} characters) to search` + ); + return; + } + showMessage(''); + + // do search + const results = searchIndex.search(term).map(match => match.ref); + + if (!results.length) { + showMessage('No results found'); + return; + } + + // Show the results + searchResults.each(function() { + const el = $(this); + if (results.includes(this.dataset.id)) { + el.css('display', 'flex'); + } else { + el.css('display', 'none'); + } + }); + + // Sort results by accuracy + searchResults + .sort(function(a, b) { + const aOrder = results.indexOf(a.dataset.id); + const bOrder = results.indexOf(b.dataset.id); + if (aOrder < bOrder) { + return -1; + } + if (aOrder > bOrder) { + return 1; + } + return 0; + }) + .appendTo(searchResultsList); + + // Show number of results + searchResultsCount.text(`Found ${results.length} results`); + searchResultsCount.show(); +} + +$(document).ready(function() { + searchInput.prop('disabled', true); + const initialSearchTerm = getInitialSearchTerm(); + searchInput.val(initialSearchTerm); + + $.getJSON('./index.json', function(data) { + // populate the index + searchIndex = lunr(function() { + this.ref('id'); + this.field('title'); + this.field('content'); + + data.forEach(function(page) { + this.add(page); + }, this); + }); + + // add event handler + searchInput.on('input', function(event) { + onSearch(event.target.value); + }); + + // re-enable the input + searchInput.prop('disabled', false); + + // Do an initial search + onSearch(searchInput.val()); + + // Scroll to the title if we landed on the page with a search term + if (initialSearchTerm) { + document.getElementsByTagName('h1')[0].scrollIntoView(); + } + }); +}); diff --git a/static/src/scss/style.scss b/static/src/scss/style.scss index 8777d2f..4861344 100644 --- a/static/src/scss/style.scss +++ b/static/src/scss/style.scss @@ -110,6 +110,17 @@ table td { #index-header { width: 100%; height: calc(100vh - #{$footer-height} - #{$navbar-height}); + + #search-input { + border: 3px solid $black; + background-color: opacify($black, 0.8); + text-align: center; + color: $body-color; + + &::placeholder { + color: opacify($body-color, 0.8); + } + } } footer { @@ -335,3 +346,23 @@ a.no-color-change { width: 100%; } } + +#main.search-page { + .search-results { + .list-page-item { + display: none; + } + } + + #search-input { + border: 0; + background-color: opacify($black, 0.6); + color: $body-color; + } + + .input-group-text { + border: 0; + background-color: $black; + color: $body-color; + } +}