From 9ed2c8d9c997b6d8c354ea022a7ae105e771d4f3 Mon Sep 17 00:00:00 2001 From: John Bowdre Date: Sat, 29 Jan 2022 16:34:29 -0600 Subject: [PATCH] implement search capability from https://github.com/onweru/compose --- assets/js/custom.js | 74 ++++++++++++- assets/sass/_custom.sass | 65 ++++++++++- config/_default/config.toml | 3 + content/search.md | 5 + i18n/en.toml | 20 +++- layouts/_default/index.json | 7 ++ layouts/partials/icons.html | 3 + layouts/partials/nav.html | 39 +++++++ layouts/partials/search.html | 12 ++ layouts/search/single.html | 5 + static/js/fuse.js | 9 ++ static/js/search.js | 205 +++++++++++++++++++++++++++++++++++ 12 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 content/search.md create mode 100644 layouts/_default/index.json create mode 100644 layouts/partials/nav.html create mode 100644 layouts/partials/search.html create mode 100644 layouts/search/single.html create mode 100644 static/js/fuse.js create mode 100644 static/js/search.js diff --git a/assets/js/custom.js b/assets/js/custom.js index bf4c405..7930eec 100644 --- a/assets/js/custom.js +++ b/assets/js/custom.js @@ -10,4 +10,76 @@ Array.from(myLabels).forEach(label => { label.click(); }; }); -}); \ No newline at end of file +}); + +// 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 = ``; + 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), ""); + }); + } +} diff --git a/assets/sass/_custom.sass b/assets/sass/_custom.sass index 0129993..572113f 100644 --- a/assets/sass/_custom.sass +++ b/assets/sass/_custom.sass @@ -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 \ No newline at end of file + 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 diff --git a/config/_default/config.toml b/config/_default/config.toml index 5a4a95c..547c093 100644 --- a/config/_default/config.toml +++ b/config/_default/config.toml @@ -10,3 +10,6 @@ DefaultContentLanguage = "en" [permalinks] posts = ":filename" + +[outputs] + home = ["HTML", "RSS","JSON"] \ No newline at end of file diff --git a/content/search.md b/content/search.md new file mode 100644 index 0000000..65759a0 --- /dev/null +++ b/content/search.md @@ -0,0 +1,5 @@ ++++ +title = "Search" +searchPage = true +type = "search" ++++ \ No newline at end of file diff --git a/i18n/en.toml b/i18n/en.toml index 20eef55..5cfd87a 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -14,4 +14,22 @@ other = "Powered by" other = "and" [view_source] -other = "View source" \ No newline at end of file +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" \ No newline at end of file diff --git a/layouts/_default/index.json b/layouts/_default/index.json new file mode 100644 index 0000000..5642b61 --- /dev/null +++ b/layouts/_default/index.json @@ -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")) -}} \ No newline at end of file diff --git a/layouts/partials/icons.html b/layouts/partials/icons.html index 852ef1a..cdadb9b 100644 --- a/layouts/partials/icons.html +++ b/layouts/partials/icons.html @@ -61,4 +61,7 @@ + + + diff --git a/layouts/partials/nav.html b/layouts/partials/nav.html new file mode 100644 index 0000000..e854819 --- /dev/null +++ b/layouts/partials/nav.html @@ -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 }} + +{{- end }} + \ No newline at end of file diff --git a/layouts/partials/search.html b/layouts/partials/search.html new file mode 100644 index 0000000..9bd4b7e --- /dev/null +++ b/layouts/partials/search.html @@ -0,0 +1,12 @@ + + + + diff --git a/layouts/search/single.html b/layouts/search/single.html new file mode 100644 index 0000000..456a268 --- /dev/null +++ b/layouts/search/single.html @@ -0,0 +1,5 @@ +{{- define "main" }} +
+
+
+{{- end }} \ No newline at end of file diff --git a/static/js/fuse.js b/static/js/fuse.js new file mode 100644 index 0000000..c3ba584 --- /dev/null +++ b/static/js/fuse.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.4.6 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2021 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + var e,t;e=this,t=function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:3,t=new Map,n=Math.pow(10,e);return{get:function(e){var r=e.match(I).length;if(t.has(r))return t.get(r);var i=1/Math.sqrt(r),o=parseFloat(Math.round(i*n)/n);return t.set(r,o),o},clear:function(){t.clear()}}}var E=function(){function e(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.getFn,i=void 0===r?A.getFn:r;t(this,e),this.norm=C(3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return r(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?A.getFn:r,o=new E({getFn:i});return o.setKeys(e.map(_)),o.setSources(t),o.create(),o}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?A.distance:s,h=t.ignoreLocation,f=void 0===h?A.ignoreLocation:h,l=r/e.length;if(f)return l;var d=Math.abs(a-o);return u?l+d/u:d?1:l}function F(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:A.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}function P(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?A.location:o,a=i.threshold,s=void 0===a?A.threshold:a,u=i.distance,h=void 0===u?A.distance:u,f=i.includeMatches,l=void 0===f?A.includeMatches:f,d=i.findAllMatches,v=void 0===d?A.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?A.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?A.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?A.ignoreLocation:k;if(t(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:l,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?n:n.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){r.chunks.push({pattern:e,alphabet:P(e),startIndex:t})},x=this.pattern.length;if(x>32){for(var L=0,S=x%32,w=x-S;L3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?A.location:i,c=r.distance,a=void 0===c?A.distance:c,s=r.threshold,u=void 0===s?A.threshold:s,h=r.findAllMatches,f=void 0===h?A.findAllMatches:h,l=r.minMatchCharLength,d=void 0===l?A.minMatchCharLength:l,v=r.includeMatches,g=void 0===v?A.includeMatches:v,y=r.ignoreLocation,p=void 0===y?A.ignoreLocation:y;if(t.length>32)throw new Error(L(32));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,S=b,w=d>1||g,_=w?Array(M):[];(m=e.indexOf(t,S))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),S=m+k,w)for(var j=0;j=K;J-=1){var T=J-1,U=n[e.charAt(T)];if(w&&(_[T]=+!!U),W[J]=(W[J+1]<<1|1)&U,P&&(W[J]|=(I[J+1]|I[J])<<1|1|I[J+1]),W[J]&$&&(C=R(t,{errors:P,currentLocation:T,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(S=T)<=b)break;K=Math.max(1,2*b-S)}}var V=R(t,{errors:P+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p});if(V>x)break;I=W}var B={isMatch:S>=0,score:Math.max(.001,C)};if(w){var G=F(_,d);G.length?g&&(B.indices=G):B.isMatch=!1}return B}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:f}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(l(d),l(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),D=function(){function e(n){t(this,e),this.pattern=n}return r(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return z(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return z(e,this.singleRegex)}}]),e}();function z(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),i}(D),q=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),i}(D),W=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),i}(D),J=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),i}(D),T=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),i}(D),U=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),i}(D),V=function(e){a(i,e);var n=f(i);function i(e){var r,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?A.location:c,s=o.threshold,u=void 0===s?A.threshold:s,h=o.distance,f=void 0===h?A.distance:h,l=o.includeMatches,d=void 0===l?A.includeMatches:l,v=o.findAllMatches,g=void 0===v?A.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?A.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?A.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?A.ignoreLocation:M;return t(this,i),(r=n.call(this,e))._bitapSearch=new N(e,{location:a,threshold:u,distance:f,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),r}return r(i,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),i}(D),B=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),i}(D),G=[K,B,W,J,U,T,q,V],H=G.length,Q=/ +(?=([^\"]*\"[^\"]*\")*[^\"]*$)/;function X(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Q).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=r.isCaseSensitive,o=void 0===i?A.isCaseSensitive:i,c=r.includeMatches,a=void 0===c?A.includeMatches:c,s=r.minMatchCharLength,u=void 0===s?A.minMatchCharLength:s,h=r.ignoreLocation,f=void 0===h?A.ignoreLocation:h,l=r.findAllMatches,d=void 0===l?A.findAllMatches:l,v=r.location,g=void 0===v?A.location:v,y=r.threshold,p=void 0===y?A.threshold:y,m=r.distance,k=void 0===m?A.distance:m;t(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:f,location:g,threshold:p,distance:k},this.pattern=o?n:n.toLowerCase(),this.query=X(this.pattern,this.options)}return r(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function le(e,t){t.score=e.score}function de(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?A.includeMatches:r,o=n.includeScore,c=void 0===o?A.includeScore:o,a=[];return i&&a.push(fe),c&&a.push(le),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ve=function(){function e(n){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2?arguments[2]:void 0;t(this,e),this.options=c({},A,{},r),this.options.useExtendedSearch,this._keyStore=new w(this.options.keys),this.setCollection(n,i)}return r(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof E))throw new Error("Incorrect 'index' type");this._myIndex=t||$(this.options.keys,this._docs,{getFn:this.options.getFn})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return he(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),de(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=te(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.auto,i=void 0===r||r,o=function e(n){var r=Object.keys(n),o=ae(n);if(!o&&r.length>1&&!ce(n))return e(ue(n));if(se(n)){var c=o?n[ie]:r[0],a=o?n[oe]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return i&&(s.searcher=te(a,t)),s}var u={children:[],operator:r[0]};return r.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return ce(e)||(e=ue(e)),o(e)}(e,this.options),r=this._myIndex.records,i={},o=[];return r.forEach((function(e){var r=e.$,c=e.i;if(k(r)){var a=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}switch(n.operator){case ne:for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?A.getFn:n,i=e.keys,o=e.records,c=new E({getFn:r});return c.setKeys(i),c.setIndexRecords(o),c},ve.config=A,function(){ee.push.apply(ee,arguments)}(Z),ve},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Fuse=t(); \ No newline at end of file diff --git a/static/js/search.js b/static/js/search.js new file mode 100644 index 0000000..0c96d96 --- /dev/null +++ b/static/js/search.js @@ -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 = `${noMatchesFound}`; + } + } else { + showResults.innerHTML = `` + } + + 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)); +}); \ No newline at end of file