From 45c89b984d3f2b3bf97c8c5ee52e62aa6a4af6dc Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Fri, 22 Jul 2022 23:27:00 +0200 Subject: Added front end search --- assets/general/elasticlunr.min.js | 10 +++ template/_includes.html | 3 +- template/_navigation.html | 2 + template/_search.html | 6 ++ template/index.html | 4 ++ template/post.html | 2 + template/script.js | 145 ++++++++++++++++++++------------------ template/style.css | 145 ++++++++++++++++++++++++++++++++------ 8 files changed, 227 insertions(+), 90 deletions(-) create mode 100644 assets/general/elasticlunr.min.js create mode 100644 template/_search.html diff --git a/assets/general/elasticlunr.min.js b/assets/general/elasticlunr.min.js new file mode 100644 index 0000000..94b20dd --- /dev/null +++ b/assets/general/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o - + + diff --git a/template/_navigation.html b/template/_navigation.html index 85290a3..45a2a9e 100755 --- a/template/_navigation.html +++ b/template/_navigation.html @@ -13,6 +13,8 @@ Books + + Search diff --git a/template/_search.html b/template/_search.html new file mode 100644 index 0000000..997b35c --- /dev/null +++ b/template/_search.html @@ -0,0 +1,6 @@ + + + diff --git a/template/index.html b/template/index.html index 8c1cc6e..cb334e1 100755 --- a/template/index.html +++ b/template/index.html @@ -55,6 +55,10 @@ {{template "_footer.html"}} + {{template "_includes.html"}} + + {{template "_search.html"}} + diff --git a/template/post.html b/template/post.html index 564ed09..cf80b22 100755 --- a/template/post.html +++ b/template/post.html @@ -69,6 +69,8 @@ {{template "_includes.html"}} + {{template "_search.html"}} + {{template "_libraries.html"}} diff --git a/template/script.js b/template/script.js index 0e1291c..3c58403 100755 --- a/template/script.js +++ b/template/script.js @@ -25,79 +25,88 @@ window.addEventListener('load', async () => { }); } - // comments code - const commentsEndpoint = 'https://mitjafelicijan.com/comments-api'; - const commentsPlaceholder = document.querySelector('.comments'); - - if (commentsPlaceholder) { - const guid = commentsPlaceholder.dataset.guid; - const name = commentsPlaceholder.querySelector('input'); - const comment = commentsPlaceholder.querySelector('textarea'); - const submit = commentsPlaceholder.querySelector('button'); - const comments = commentsPlaceholder.querySelector('ul'); - - if (guid) { - await readAndRenderComments(guid, comments); - - submit.addEventListener('click', async() => { - submit.disabled = true; - await writeComments(guid, name.value, comment.value); - - submit.disabled = false; - name.value = ''; - comment.value = ''; - - await readAndRenderComments(guid, comments); - }); - } + // Search functionality + + window.index = null; + + const response = await fetch('/feed.json'); + const feed = await response.json(); + + window.index = elasticlunr(function () { + this.addField('title'); + this.addField('body'); + this.setRef('id'); + }); + + for (const item of feed.items) { + item.id = item.url; + window.index.addDoc({ + id: item.url, + title: item.title, + body: item.content_html, + url: item.url, + }); } - async function writeComments(guid, name, comment) { - const response = await fetch(commentsEndpoint, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - action: 'write', - guid, - name, - comment, - }) - }); + const blur = document.querySelector('.blur'); + const searchForm = document.querySelector('.search-form'); + const searchResultsList = document.querySelector('.search-form ul'); + + function showSearchModal() { + blur.classList.remove('hidden'); + searchForm.classList.remove('hidden'); + + // Clear search input. + searchForm.querySelector('input').value = ''; + + // We need to clear the list before opening modal. + searchResultsList.innerHTML = ''; + + // Focus on search input. + searchForm.querySelector('input').focus(); } - async function readAndRenderComments(guid, commentsPlaceholder) { - const response = await fetch(commentsEndpoint, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - action: 'read', - guid, - }) - }); + document.querySelector('.search-trigger').addEventListener('click', async (evt) => { + showSearchModal(); + }); + + document.onkeydown = function (e) { + // Show search modal on F key. + if (blur.classList.contains('hidden')) { + if (e.key === 'f') { + setTimeout(() => { + showSearchModal(); + }, 100); + } + } - // remove all existing comments from list - commentsPlaceholder.innerHTML = ''; - - const commentList = await response.json(); - for (const comment of commentList.reverse()) { - const date = new Date(comment.date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric' - }); - - const commentElement = document.createElement('li'); - commentElement.innerHTML = `

${comment.name} - ${date}

${comment.comment}


`; - commentsPlaceholder.appendChild(commentElement); + // Hide search modal on escape key. + if (!blur.classList.contains('hidden')) { + if (e.key === 'Escape') { + blur.classList.add('hidden'); + searchForm.classList.add('hidden'); + } } - } + }; + + blur.addEventListener('click', async (evt) => { + evt.target.classList.add('hidden'); + searchForm.classList.add('hidden'); + }); + + document.querySelector('.search-form input').addEventListener('keyup', async (evt) => { + // Perform search. + const searchResults = window.index.search(evt.target.value); + + // We need to clear the list before adding new results. + searchResultsList.innerHTML = ''; + + // Loop through the results and add them to the list. + for (const result of searchResults.slice(0, 9)) { + const listItem = document.createElement('li'); + listItem.innerHTML = `${result.doc.title}`; + searchResultsList.appendChild(listItem); + } + }); }); diff --git a/template/style.css b/template/style.css index af0d7bc..ebb2b26 100755 --- a/template/style.css +++ b/template/style.css @@ -29,7 +29,7 @@ body { background: white; /*font-family: 'Times New Roman', Times, serif;*/ /*font-family: 'IBM Plex Sans', sans-serif;*/ - font-family: "SF Pro Text","SF Pro Icons","Helvetica Neue","Helvetica","Arial",sans-serif; + font-family: "SF Pro Text", "SF Pro Icons", "Helvetica Neue", "Helvetica", "Arial", sans-serif; color: var(--base-color); font-size: var(--base-font-size); line-height: var(--base-line-heigh); @@ -54,6 +54,10 @@ hr { color: #000; } +.cursor { + cursor: pointer; +} + /* width of the page */ .wrapper { @@ -74,12 +78,35 @@ a:hover { /* headings */ -h1 { font-size: 220%; line-height: 1.2em; } -h2 { font-size: 180%; line-height: 1.2em; } -h3 { font-size: 160%; line-height: 1.2em; } -h4 { font-size: 140%; line-height: 1.2em; } -h5 { font-size: 120%; line-height: 1.2em; } -h6 { font-size: 100%; line-height: 1.2em; } +h1 { + font-size: 220%; + line-height: 1.2em; +} + +h2 { + font-size: 180%; + line-height: 1.2em; +} + +h3 { + font-size: 160%; + line-height: 1.2em; +} + +h4 { + font-size: 140%; + line-height: 1.2em; +} + +h5 { + font-size: 120%; + line-height: 1.2em; +} + +h6 { + font-size: 100%; + line-height: 1.2em; +} h1[itemtype="headline"] { padding-bottom: 0; @@ -95,12 +122,15 @@ table { width: 100%; } -table, th, td { +table, +th, +td { border: 1px solid black; text-align: left; } -th, td { +th, +td { padding: 5px 10px; } @@ -268,7 +298,7 @@ code { font-weight: 500; } -pre > code { +pre>code { background: unset; padding: unset; @@ -285,7 +315,8 @@ pre { margin-block-end: 40px; } -img, video { +img, +video { max-width: 100%; margin: 30px auto; display: block; @@ -357,14 +388,14 @@ audio { /* comments */ -.comments input{ +.comments input { width: 100%; font: var(--comment-form-font); border: 1px solid #bbb; padding: 5px; } -.comments textarea{ +.comments textarea { width: 100%; height: 100px; resize: vertical; @@ -384,13 +415,69 @@ audio { border-top: initial !important; } +/* search form */ + +.search-form { + position: fixed; + top: 120px; + left: 50%; + margin-left: -250px; + width: 500px; + padding: 30px; + background: #eee; + border-radius: 5px; +} + +.blur { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + backdrop-filter: blur(15px); +} + +.hidden { + display: none; +} + +.search-form input { + width: 100%; + margin-bottom: 20px; + border: 1px solid #ffffff; + padding: 5px 10px; + border-radius: 3px; + outline: none; +} + +.search-form ul { + list-style-type: none; + padding: 0; + margin: 0; +} + +.search-form ul li { + margin-bottom: 5px; +} + /* responsive */ @media only screen and (max-width: 960px) { - main { padding: 0 20px; } - footer { padding: 0 20px; } - h1[itemtype="headline"] { font-size: 220%; } - .navigation header { padding: 0 20px; } + main { + padding: 0 20px; + } + + footer { + padding: 0 20px; + } + + h1[itemtype="headline"] { + font-size: 220%; + } + + .navigation header { + padding: 0 20px; + } article img { max-width: 100%; @@ -400,10 +487,26 @@ audio { } @media only screen and (max-width: 600px) { - .navigation header { display: block; } - .navigation header h3 { text-align: center; margin-bottom: 10px; } - .navigation header nav { text-align: center; } - .post-list li a h2 { font-weight: 500; } + .navigation header { + display: block; + } + + .navigation header h3 { + text-align: center; + margin-bottom: 10px; + } + + .navigation header nav { + text-align: center; + } + + .post-list li a h2 { + font-weight: 500; + } + + .search-trigger { + display: none; + } } /* light/dark mode */ -- cgit v1.2.3