Search: Refactor, refine history behaviour, add breadcrumbs

This commit is contained in:
Phaiax 2017-10-03 21:52:56 +02:00
parent 379c6ff616
commit c487a95d24
2 changed files with 310 additions and 202 deletions

View File

@ -212,6 +212,17 @@ ul#searchresults {
ul#searchresults li { ul#searchresults li {
margin: 10px 0px; margin: 10px 0px;
} }
ul#searchresults span.breadcrumbs {
float: right;
color: #CCC;
font-size: 0.9em;
margin-left: 10px;
}
ul#searchresults span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
}
.menu-title { .menu-title {
position: absolute; position: absolute;
display: block; display: block;

View File

@ -1,179 +1,329 @@
$( document ).ready(function() { $( document ).ready(function() {
// Helpers for searching // Search functionality
function create_test_searchindex() { //
var searchindex = elasticlunr(function () { // Usage: call init() on startup. You can use hasFocus() to disable prevent keyhandling
this.addField('body'); // while the user is typing his search.
this.addField('title'); var search = {
this.setRef('id'); searchbar : $('#searchbar'),
}); searchbar_outer : $('#searchbar-outer'),
var content = $("#content"); searchresults : $('#searchresults'),
var paragraphs = content.children(); searchresults_outer : $("#searchresults-outer"),
var curr_title = ""; searchresults_header : $("#searchresults-header"),
var curr_body = ""; searchicon : $("#search-icon"),
var curr_ref = ""; content : $('#content'),
var push = function(ref) {
if ((curr_title.length > 0 || curr_body.length > 0) && curr_ref.length > 0) {
var doc = {
"id": curr_ref,
"body": curr_body,
"title": curr_title
}
searchindex.addDoc(doc);
}
curr_body = "";
curr_title = "";
curr_ref = "";
};
paragraphs.each(function(index, element) {
// todo uppercase
var el = $(element);
if (el.prop('nodeName').toUpperCase() == "A") {
// new header, push old paragraph to index
push(index);
curr_title = el.text();
curr_ref = el.attr('href');
} else {
curr_body += " \n " + el.text();
}
// last paragraph
if (index == paragraphs.length - 1) {
push(index);
}
});
return searchindex;
}
function parseURL(url) { searchindex : null,
var a = document.createElement('a'); searchoptions : {
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':',''),
host: a.hostname,
port: a.port,
params: (function(){
var ret = {};
var seg = a.search.replace(/^\?/,'').split('&');
var len = seg.length, i = 0, s;
for (;i<len;i++) {
if (!seg[i]) { continue; }
s = seg[i].split('=');
ret[s[0]] = s[1];
}
return ret;
})(),
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
hash: a.hash.replace('#',''),
path: a.pathname.replace(/^([^/])/,'/$1')
};
}
function renderURL(urlobject) {
var url = urlobject.protocol + "://" + urlobject.host;
if (urlobject.port != "") {
url += ":" + urlobject.port;
}
url += urlobject.path;
var joiner = "?";
for(var prop in urlobject.params) {
if(urlobject.params.hasOwnProperty(prop)) {
url += joiner + prop + "=" + urlobject.params[prop];
joiner = "&";
}
}
if (urlobject.hash != "") {
url += "#" + urlobject.hash;
}
return url;
}
var current_searchterm = "";
var teaser_size_half = 80;
function doSearch(searchindex, searchterm) {
var display = $('#searchresults');
// Don't search twice the same
if (current_searchterm == searchterm) { return; }
else { current_searchterm = searchterm; }
// Do the actual search
var results = searchindex.search(searchterm, {
bool: "AND", bool: "AND",
expand: true expand: true,
}); fields: {
title: {boost: 1},
body: {boost: 1},
breadcrumbs: {boost: 0}
}
},
mark_exclude : [], // ['.hljs']
current_searchterm : "",
teaser_size_half : 80,
resultcount_limit : 30,
SEARCH_PARAM : 'search',
MARK_PARAM : 'highlight',
// Display search metrics SEARCH_HOTKEY_KEYCODE: 83,
var searchheader = ""; ESCAPE_KEYCODE: 27,
if (results.length > 0) {
searchheader = results.length + " search results for '" + searchterm + "':"; formatSearchMetric : function(count, searchterm) {
} else if (results.length == 1) { if (count == 1) {
searchheader = results.length + " search result for '" + searchterm + "':"; return count + " search result for '" + searchterm + "':";
} else { } else if (count == 0) {
searchheader = "No search results for '" + searchterm + "'."; return "No search results for '" + searchterm + "'.";
} else {
return count + " search results for '" + searchterm + "':";
}
} }
$('#searchresults-header').text(searchheader); ,
create_test_searchindex : function () {
// Clear and insert results var searchindex = elasticlunr(function () {
var firstterm = searchterm.split(' ')[0]; this.addField('body');
display.empty(); this.addField('title');
for(var i = 0, size = results.length; i < size ; i++){ this.addField('breadcrumbs')
var result = results[i]; this.setRef('id');
var firstoccurence = result.doc.body.search(firstterm); });
var base_breadcrumbs = "";
var active_chapter = $('.sidebar ul a.active');
base_breadcrumbs = active_chapter.text().split('. ', 2)[1]; // demo
while (true) {
var parent_ul = active_chapter.parents('ul');
if (parent_ul.length == 0) break;
var parent_li = parent_ul.parents('li');
if (parent_li.length == 0) break;
var pre_li = parent_li.prev('li');
if (pre_li.length == 0) break;
base_breadcrumbs = pre_li.text().split('. ', 2)[1] + ' » ' + base_breadcrumbs;
active_chapter = pre_li;
}
var paragraphs = this.content.children();
var curr_title = "";
var curr_body = "";
var curr_ref = "";
var push = function(ref) {
if ((curr_title.length > 0 || curr_body.length > 0) && curr_ref.length > 0) {
var doc = {
"id": curr_ref,
"body": curr_body,
"title": curr_title,
"breadcrumbs": base_breadcrumbs //"Header1 » Header2"
}
searchindex.addDoc(doc);
}
curr_body = "";
curr_title = "";
curr_ref = "";
};
paragraphs.each(function(index, element) {
// todo uppercase
var el = $(element);
if (el.prop('nodeName').toUpperCase() == "A") {
// new header, push old paragraph to index
push(index);
curr_title = el.text();
curr_ref = el.attr('href');
} else {
curr_body += " \n " + el.text();
}
// last paragraph
if (index == paragraphs.length - 1) {
push(index);
}
});
this.searchindex = searchindex;
}
,
parseURL : function (url) {
var a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':',''),
host: a.hostname,
port: a.port,
params: (function(){
var ret = {};
var seg = a.search.replace(/^\?/,'').split('&');
var len = seg.length, i = 0, s;
for (;i<len;i++) {
if (!seg[i]) { continue; }
s = seg[i].split('=');
ret[s[0]] = s[1];
}
return ret;
})(),
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
hash: a.hash.replace('#',''),
path: a.pathname.replace(/^([^/])/,'/$1')
};
}
,
renderURL : function (urlobject) {
var url = urlobject.protocol + "://" + urlobject.host;
if (urlobject.port != "") {
url += ":" + urlobject.port;
}
url += urlobject.path;
var joiner = "?";
for(var prop in urlobject.params) {
if(urlobject.params.hasOwnProperty(prop)) {
url += joiner + prop + "=" + urlobject.params[prop];
joiner = "&";
}
}
if (urlobject.hash != "") {
url += "#" + urlobject.hash;
}
return url;
}
,
formatSearchResult : function (result, searchterms) {
// Show text around first occurrence of first search term.
var firstoccurence = result.doc.body.search(searchterms[0]);
var teaser = ""; var teaser = "";
if (firstoccurence != -1) { if (firstoccurence != -1) {
var teaserstartindex = firstoccurence - teaser_size_half; var teaserstartindex = firstoccurence - this.teaser_size_half;
var nextwordindex = result.doc.body.indexOf(" ", teaserstartindex); var nextwordindex = result.doc.body.indexOf(" ", teaserstartindex);
if (nextwordindex != -1) { if (nextwordindex != -1) {
teaserstartindex = nextwordindex; teaserstartindex = nextwordindex;
} }
var teaserendindex = firstoccurence + teaser_size_half; var teaserendindex = firstoccurence + this.teaser_size_half;
nextwordindex = result.doc.body.indexOf(" ", teaserendindex); nextwordindex = result.doc.body.indexOf(" ", teaserendindex);
if (nextwordindex != -1) { if (nextwordindex != -1) {
teaserendindex = nextwordindex; teaserendindex = nextwordindex;
} }
teaser = (teaserstartindex > 0) ? "..." : ""; teaser = (teaserstartindex > 0) ? "... " : "";
teaser += result.doc.body.substring(teaserstartindex, teaserendindex) + "..."; teaser += result.doc.body.substring(teaserstartindex, teaserendindex) + " ...";
} else { } else {
teaser = result.doc.body.substr(0, 80) + "..."; teaser = result.doc.body.substr(0, this.teaser_size_half * 2) + " ...";
} }
// The ?MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
var url = result.ref.split("#"); var url = result.ref.split("#");
if (url.length == 1) { if (url.length == 1) {
url.push(""); url.push("");
} }
display.append('<li><a href="' + url[0] + '?highlight=' + searchterm + '#' + url[1] + '">' return $('<li><a href="'
+ result.doc.title + '</a>: ' + teaser + "</li>"); + url[0] + '?' + this.MARK_PARAM + '=' + searchterms + '#' + url[1]
+ '">' + result.doc.title + '</a>'
+ '<span class="breadcrumbs">' + result.doc.breadcrumbs + '</span>'
+ '<span class="teaser">' + teaser + '</span>'
+ '</li>');
} }
,
doSearch : function (searchterm) {
// Display and scroll to results // Don't search the same twice
$("#menu-bar").scrollTop(0); if (this.current_searchterm == searchterm) { return; }
$("#searchresults-outer").slideDown(); else { this.current_searchterm = searchterm; }
}
function doSearchOrHighlightFromUrl() { if (this.searchindex == null) { return; }
// Check current URL for search request
var url = parseURL(window.location.href); // Do the actual search
if (url.params.hasOwnProperty('search')) { var results = this.searchindex.search(searchterm, this.searchoptions);
$("#searchbar-outer").slideDown(); var resultcount = (results.length > this.resultcount_limit)
$("#searchbar")[0].value = url.params['search']; ? this.resultcount_limit : results.length;
$("#searchbar").trigger('keyup');
} else { // Display search metrics
$("#searchbar-outer").slideUp(); this.searchresults_header.text(this.formatSearchMetric(resultcount, searchterm));
// Clear and insert results
var searchterms = searchterm.split(' ');
this.searchresults.empty();
for(var i = 0; i < resultcount ; i++){
this.searchresults.append(this.formatSearchResult(results[i], searchterms));
}
// Display and scroll to results
this.searchresults_outer.slideDown();
// this.searchicon.scrollTop(0);
} }
,
doSearchOrMarkFromUrl : function () {
// Check current URL for search request
var url = this.parseURL(window.location.href);
if (url.params.hasOwnProperty(this.SEARCH_PARAM)
&& url.params[this.SEARCH_PARAM] != "") {
this.searchbar_outer.slideDown();
this.searchbar[0].value = url.params[this.SEARCH_PARAM];
this.searchbarKeyUpHandler();
} else {
this.searchbar_outer.slideUp();
}
if (url.params.hasOwnProperty('highlight')) { if (url.params.hasOwnProperty(this.MARK_PARAM)) {
var words = url.params['highlight'].split(' '); var words = url.params[this.MARK_PARAM].split(' ');
var header = $('#' + url.hash); var header = $('#' + url.hash);
$('.content').mark(words, { this.content.mark(words, {
// exclude : ['.hljs'] exclude : this.mark_exclude
}); });
}
} }
} ,
init : function () {
// For testing purposes: Index current page
this.create_test_searchindex();
// Set up events
var this_ = this;
this.searchicon.click( function(e) { this_.searchIconClickHandler(); } );
this.searchbar.on('keyup', function(e) { this_.searchbarKeyUpHandler(); } );
$(document).on('keydown', function (e) { this_.globalKeyHandler(e); });
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = function(e) { this_.doSearchOrMarkFromUrl(); };
// If reloaded, do the search or mark again, depending on the current url parameters
this.doSearchOrMarkFromUrl();
}
,
hasFocus : function () {
return this.searchbar.is(':focus');
}
,
globalKeyHandler : function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (e.keyCode == this.ESCAPE_KEYCODE) {
e.preventDefault();
this.searchbar.removeClass("active");
// this.searchbar[0].value = "";
this.setSearchUrlParameters("",
(this.searchbar[0].value.trim() != 0) ? "push" : "replace");
this.unfocusSearchbar();
this.searchbar_outer.slideUp();
return;
}
if (!this.hasFocus() && e.keyCode == this.SEARCH_HOTKEY_KEYCODE) {
e.preventDefault();
this.searchbar_outer.slideDown()
this.searchbar.focus();
}
}
,
unfocusSearchbar : function () {
// hacky, but just focusing a div only works once
var tmp = $('<input style="position: absolute; opacity: 0;">');
tmp.insertAfter(this.searchicon);
tmp.focus();
tmp.remove();
}
,
searchIconClickHandler : function () {
this.searchbar_outer.slideToggle();
this.searchbar.focus();
// TODO:
// If invisible, clear URL search parameter
}
,
searchbarKeyUpHandler : function () {
var searchterm = this.searchbar[0].value.trim();
if (searchterm != "") {
this.searchbar.addClass("active");
this.doSearch(searchterm);
} else {
this.searchbar.removeClass("active");
this.searchresults_outer.slideUp();
this.searchresults.empty();
}
this.setSearchUrlParameters(searchterm, "if_begin_search");
// Remove marks
this.content.unmark();
}
,
setSearchUrlParameters : function(searchterm, action) {
// Update url with ?SEARCH_PARAM= parameter, remove ?MARK_PARAM and #heading-anchor
var url = this.parseURL(window.location.href);
var first_search = ! url.params.hasOwnProperty(this.SEARCH_PARAM);
if (searchterm != "" || action == "if_begin_search") {
url.params[this.SEARCH_PARAM] = searchterm;
delete url.params[this.MARK_PARAM];
url.hash = "";
} else {
delete url.params[this.SEARCH_PARAM];
}
// A new search will also add a new history item, so the user can go back
// to the page prior to searching. A updated search term will only replace
// the url.
if (action == "push" || (action == "if_begin_search" && first_search) ) {
history.pushState({}, document.title, this.renderURL(url));
} else if (action == "replace" || (action == "if_begin_search" && !first_search) ) {
history.replaceState({}, document.title, this.renderURL(url));
}
}
};
// Interesting DOM Elements
var sidebar = $("#sidebar");
// url // url
var url = window.location.pathname; var url = window.location.pathname;
@ -213,13 +363,12 @@ $( document ).ready(function() {
var KEY_CODES = { var KEY_CODES = {
PREVIOUS_KEY: 37, PREVIOUS_KEY: 37,
NEXT_KEY: 39, NEXT_KEY: 39
SEARCH_KEY: 83
}; };
$(document).on('keydown', function (e) { $(document).on('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if ($('#searchbar').is( ":focus" )) { return; } if (search.hasFocus()) { return; }
switch (e.keyCode) { switch (e.keyCode) {
case KEY_CODES.NEXT_KEY: case KEY_CODES.NEXT_KEY:
e.preventDefault(); e.preventDefault();
@ -233,17 +382,9 @@ $( document ).ready(function() {
window.location.href = $('.nav-chapters.previous').attr('href'); window.location.href = $('.nav-chapters.previous').attr('href');
} }
break; break;
case KEY_CODES.SEARCH_KEY:
e.preventDefault();
$("#searchbar-outer").slideDown()
$('#searchbar').focus();
break;
} }
}); });
// Interesting DOM Elements
var sidebar = $("#sidebar");
// Help keyboard navigation by always focusing on page content // Help keyboard navigation by always focusing on page content
$(".page").focus(); $(".page").focus();
@ -264,52 +405,8 @@ $( document ).ready(function() {
sidebar.scrollTop(activeSection.offset().top); sidebar.scrollTop(activeSection.offset().top);
} }
// For testing purposes: Index current page // Search
var searchindex = create_test_searchindex(); search.init();
$("#search-icon").click(function(e) {
var outer = $("#searchbar-outer");
outer.slideToggle();
// TODO:
// If invisible, clear URL search parameter
});
// Searchbar
$("#searchbar").on('keyup', function (e) {
var display = $('#searchresults');
var outer = $("#searchresults-outer");
var searchterm = e.target.value.trim();
if (searchterm != "") {
$(e.target).addClass("active");
doSearch(searchindex, searchterm);
} else {
$(e.target).removeClass("active");
outer.slideUp();
display.empty();
}
var url = parseURL(window.location.href);
var first_search = ! url.params.hasOwnProperty("search");
url.params["search"] = searchterm;
delete url.params["highlight"];
url.hash = "";
if (first_search) {
history.pushState({}, document.title, renderURL(url));
} else {
history.replaceState({}, document.title, renderURL(url));
}
$('.content').unmark();
});
window.onpopstate = function(e) {
doSearchOrHighlightFromUrl();
};
doSearchOrHighlightFromUrl();
// Theme button // Theme button
$("#theme-toggle").click(function(){ $("#theme-toggle").click(function(){