// in this global variable the json object will be saved var searchJson = null; // This function loads the search.json, parses it and saves it in the searchJson variable. function loadSearchJson() { var req = new XMLHttpRequest(); req.addEventListener('load', function() { searchJson = JSON.parse(this.responseText); for(var i=0; i < searchJson.length; i++) if(searchJson[i].title == "") searchJson.splice(i--, 1); }); req.open("GET", baseUrl + "search.json"); req.send(); } // It is called immediately loadSearchJson(); // When the DOM is ready the search input field is prepared. window.addEventListener('DOMContentLoaded', function() { // The text input gets an input handler that calls the search function with the input string document.querySelector('.nav-list-search').addEventListener('input', function() { search(this.value) }); }); function createElement(params) { if(!params.name) return null; var node = document.createElement(params.name); if(x = params.id) node.id = x; if(x = params.classes) node.className = x; if(x = params.href) node.href = x; if(x = params.innerText) node.innerText = x; if(x = params.innerHTML) node.innerHTML = x; if(x = params.parent) x.appendChild(node); if(x = params.childs) { if(typeof x.forEach !== 'function') x = [].slice.call(x); x.forEach(function(child) { node.appendChild(child); }); } return node; } var Range = function(start, end) { this.start = start, this.end = end } Range.prototype.intersects = function(range2) { return (range2.end >= this.start && range2.start <= this.start) || (range2.start <= this.end && range2.end >= this.end) || (range2.start <= this.start && range2.end >= this.end) || (range2.start > this.start && range2.end < this.end); } Range.prototype.equals = function(range2) { return (this.start == range2.start && this.end == range2.end); } Range.prototype.combine = function(range2) { return new Range(Math.min(this.start, range2.start), Math.max(this.end, range2.end)); } Range.combineArray = function(ranges) { if(ranges.length == 0) return []; var resultRanges = [ranges[0]]; for(var j=1; j < ranges.length; j++) { var newRange = true; for(var k=0; k < resultRanges.length; k++) { if(ranges[j].equals(resultRanges[k])) { newRange = false; break; } if(ranges[j].intersects(resultRanges[k])) { ranges[j] = ranges[j].combine(resultRanges[k]); resultRanges.splice(k, 1); resultRanges.push(ranges[j]); for(var l=0; l < resultRanges.length; l++) { if(l == k) continue; if(resultRanges[l].intersects(resultRanges[k])) { resultRanges[k] = resultRanges[k].combine(resultRanges[l]); resultRanges.splice(l--, 1); } } newRange = false; break; } } if(newRange) resultRanges.push(ranges[j]); } return resultRanges; } Range.sortArray = function(ranges) { ranges.sort(function(a, b) { if(a.start > b.start) return 1; if(a.start < b.start) return -1; return 0; }); } function search(query) { // If the search json is not loaded yet do nothing. if(searchJson == null) return; // Get the result list element and clear it. var resultList; if(resultList = document.querySelector('ul.home.post-list.search-result')) resultList.innerHTML = ''; // If no query was entered 'unhide' the main content and paginator and return if(query == "") { if(x = document.querySelector('.main-content')) x.classList.remove('hide'); if(x = document.querySelector('.paginator')) x.classList.remove('hide'); return; } // .. else hide them. if(x = document.querySelector('.main-content')) x.classList.add('hide'); if(x = document.querySelector('.paginator')) x.classList.add('hide'); // convert the query to lower case to make the search case-insensitive query = query.toLowerCase(); // use the superSplit method to split the query in all possible combinations var splitSplitQuery = query.superSplit(" "); // calculate the score of each post for(var i=0; i < searchJson.length; i++) { var post = searchJson[i]; post.score = 0; post.contentText = createElement({ name: 'div', innerHTML: post.content.toLowerCase() }).innerText; // loop through all parts of the query for(var j=0; j < splitSplitQuery.length; j++) { var splitQuery = splitSplitQuery[j]; for(var k=0; k < splitQuery.length; k++) { var queryPart = splitQuery[k]; var titleIndexes = post.title.toLowerCase().superIndexOf(queryPart); post.score += Math.pow(splitSplitQuery.length - j, 2) * titleIndexes.length * 100 var contentIndexes = post.content.toLowerCase().superIndexOf(queryPart); post.score += Math.pow(splitSplitQuery.length - j, 2) * contentIndexes.length; // check for categories and tags only when fully split if(j == splitSplitQuery.length - 1) { for(var l=0; l < post.categories.length; l++) { if(post.categories[l].toLowerCase().indexOf(queryPart) != -1) post.score += 2000; } for(var l=0; l < post.tags.length; l++) { if(post.tags[l].toLowerCase().indexOf(queryPart) != -1) post.score += 1000; } } } } } // sort the posts according to their score searchJson.sort(function(a, b) { if(a.score < b.score) return 1; if(a.score > b.score) return -1; return 0; }); // create the search result list element if(!resultList) { resultList = createElement({ name: 'ul', classes: 'home post-list search-result', parent: document.querySelector('section.container') }); } // create the html code for each post for(var i=0; i < searchJson.length; i++) { // don't show the post if the score is 0 if(searchJson[i].score == 0) break; var contentText = searchJson[i].contentText; var ranges = []; // search for the first appearance of each keyword in the posts content // and add a range from 100 characters before and after the keyword splitSplitQuery.lastElement().forEach(function(keyword) { var index = contentText.indexOf(keyword); ranges.push(new Range(Math.max(index - 100, 0), Math.min(index + keyword.length + 100, contentText.length))); }); // combine the ranges var combinedRanges = Range.combineArray(ranges); var resultContentText = ""; // in each of the remaining ranges mark all keywords for(var j=0; j < combinedRanges.length; j++) { var part = contentText.substring(combinedRanges[j].start, combinedRanges[j].end); var markRanges = []; splitSplitQuery.lastElement().forEach(function(keyword) { var indexes = part.superIndexOf(keyword); for(var k=0; k < indexes.length; k++) markRanges.push(new Range(indexes[k], indexes[k] + keyword.length)); }); var combinedMarkRanges = Range.combineArray(markRanges); Range.sortArray(combinedMarkRanges); var offset = 0; for(var k=0; k < combinedMarkRanges.length; k++) { part = part.insert(combinedMarkRanges[k].start + offset, ""); offset += "".length; part = part.insert(combinedMarkRanges[k].end + offset, ""); offset += "".length; } resultContentText += "
" + part + "
\n"; } // create the code for the post createElement({ name: 'li', classes: 'post-list-item', parent: resultList, childs: [createElement({ name: 'article', classes: 'post-block', childs: [ createElement({ name: 'h2', classes: 'post-title', childs: [createElement({ name: 'a', classes: 'post-title-link', href: searchJson[i].url, innerText: searchJson[i].title, })] }), createElement({ name: 'div', classes: 'post-content', innerHTML: resultContentText, }) ] })] }); } // if no post was found display a "-1" if(i == 0) { createElement({ name: 'li', classes: 'post-list-item', parent: resultList, childs: [createElement({ name: 'article', classes: 'post-block', childs: [createElement({ name: 'h3', classes: 'nothing-found', innerText: '-1' })] })] }); } } Array.prototype.superJoin = function(seperator, start, length) { var result = ""; for(var i = start; i < this.length-1 && i < start+length-1; i++) result += this[i] + seperator; result += this[i]; return result; } Array.prototype.lastElement = function() { return this[this.length-1]; } String.prototype.superSplit = function(seperator) { var split = this.split(seperator); for(var i=0; i