Skip to content

Commit

Permalink
Add quick search
Browse files Browse the repository at this point in the history
  • Loading branch information
bterlson committed Oct 21, 2015
1 parent d4ecf14 commit 03d1c14
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 3 deletions.
32 changes: 31 additions & 1 deletion css/elements.css
Original file line number Diff line number Diff line change
Expand Up @@ -472,14 +472,44 @@ del.block {
color: #B6C8E4;
}

#menu-search {
color: #B6C8E4;
}

#menu-search-box {
display: block;
width: 90%;
margin: 5px auto;
font-size: 1em;
padding: 2px;
}

#menu-search-results.inactive {
display: none;
}

#menu-search-results ul {
list-style-type: square;
padding: 0 0 0 35px;
margin: 0;
}

#menu-search-results li {
white-space: nowrap;
}

#menu-search-results a {
color: #b6c8e4;
}

@media (max-width: 1366px) {
body {
margin: 0 0 0 150px;
}

#menu {
display: none;
padding-top: 2em;
padding-top: 3em;
width: 323px;
}

Expand Down
207 changes: 206 additions & 1 deletion js/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,29 @@
function Menu() {
this.$toggle = document.getElementById('menu-toggle');
this.$menu = document.getElementById('menu');
this.$searchBox = document.getElementById('menu-search-box');
this.$searchResults = document.getElementById('menu-search-results');
this.initSearch();

this.$toggle.addEventListener('click', this.toggle.bind(this));

this.$searchBox.addEventListener('keydown', function (e) {
if (e.keyCode === 191 && e.target.value.length === 0) {
e.preventDefault();
e.stopPropagation();
} else if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
this.selectResult();
}
}.bind(this));

this.$searchBox.addEventListener('keyup', debounce(function (e) {
e.stopPropagation();
this.search(e.target.value);
}.bind(this)));


var tocItems = this.$menu.querySelectorAll('#menu-toc li');
for (var i = 0; i < tocItems.length; i++) {
var $item = tocItems[i];
Expand All @@ -26,11 +46,196 @@ function Menu() {
}

Menu.prototype.toggle = function () {
this.$menu.classList.toggle("active");
this.$menu.classList.toggle('active');
}

Menu.prototype.show = function () {
this.$menu.classList.add('active');
}

Menu.prototype.hide = function () {
this.$menu.classList.remove('active');
}

Menu.prototype.isVisible = function() {
return this.$menu.classList.contains('active');
}

Menu.prototype.initSearch = function () {
var $biblio = document.getElementById('menu-search-biblio');
if (!$biblio) {
this.biblio = {};
} else {
this.biblio = JSON.parse($biblio.textContent);
}

document.addEventListener('keydown', function (e) {
if (e.keyCode === 191) {
e.preventDefault();
e.stopPropagation();

if(this.isVisible()) {
this._closeAfterSearch = false;
} else {
this._closeAfterSearch = true;
this.show();
}

this.show();
this.$searchBox.focus();
}
}.bind(this))
}

Menu.prototype.search = function (needle) {
if (needle.length < 2) {
this.hideSearch();
} else {
this.showSearch();
}

needle = needle.toLowerCase();

var results = {};
var seenClauses = {};

results.ops = Object.keys(this.biblio.ops).map(function (k) {
return this.biblio.ops[k];
}.bind(this)).filter(function(op) {
return fuzzysearch(needle, op.aoid.toLowerCase());
});

results.ops.forEach(function(op) {
seenClauses[op.id] = true;
});

results.productions = Object.keys(this.biblio.productions).map(function (k) {
return this.biblio.productions[k];
}.bind(this)).filter(function(prod) {
return fuzzysearch(needle, prod.name.toLowerCase());
});

results.clauses = Object.keys(this.biblio.clauses).map(function (k) {
return this.biblio.clauses[k];
}.bind(this)).filter(function(clause) {
return !seenClauses[clause.id] && (clause.number.indexOf(needle) === 0 || fuzzysearch(needle, clause.title.toLowerCase()));
});

if (results.length > 50) {
results = results.slice(0, 50);
}

this.displayResults(results);
}

Menu.prototype.displayResults = function (results) {
var totalResults = Object.keys(results).reduce(function (sum, record) { return sum + record.length }, 0);

if (totalResults > 0) {
this.$searchResults.classList.remove('no-results');

var html = '<ul>';

results.ops.forEach(function (op) {
html += '<li class=menu-search-result-op><a href="#' + op.id + '">' + op.aoid + '</a></li>'
});

results.productions.forEach(function (prod) {
html += '<li class=menu-search-result-prod><a href="#' + prod.id + '">' + prod.name + '</a></li>'
});

results.clauses.forEach(function (clause) {
html += '<li class=menu-search-result-clause><a href="#' + clause.id + '">' + clause.number + ' ' + clause.title + '</a></li>'
})

html += '</ul>'

this.$searchResults.innerHTML = html;
} else {
this.$searchResults.classList.add('no-results');
}
}

Menu.prototype.hideSearch = function () {
this.$searchResults.classList.add('inactive');
}

Menu.prototype.showSearch = function () {
this.$searchResults.classList.remove('inactive');
}

Menu.prototype.selectResult = function () {
var $first = this.$searchResults.querySelector('li:first-child a');

if ($first) {
document.location = $first.getAttribute('href');
}

this.$searchBox.value = '';
this.$searchBox.blur();
this.hideSearch();

if (this._closeAfterSearch) {
this.hide();
}
}

function init() {
var menu = new Menu();
}

document.addEventListener('DOMContentLoaded', init);

function debounce(fn) {
var timeout;
return function() {
var args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function() {
timeout = null;
fn.apply(this, args);
}.bind(this), 150);
}
}

// The following license applies to the fuzzysearch function
// The MIT License (MIT)
// Copyright © 2015 Nicolas Bevacqua
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
function fuzzysearch (needle, haystack) {
var tlen = haystack.length;
var qlen = needle.length;
if (qlen > tlen) {
return false;
}
if (qlen === tlen) {
return needle === haystack;
}
outer: for (var i = 0, j = 0; i < qlen; i++) {
var nch = needle.charCodeAt(i);
while (j < tlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer;
}
}
return false;
}
return true;
}
13 changes: 13 additions & 0 deletions lib/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ const Toc = require('./Toc');
module.exports = class Menu extends Builder {
build() {
const toc = Toc.build(this.spec, true);

const tocContainer = this.spec.doc.createElement('div');
tocContainer.setAttribute('id', 'menu-toc');
tocContainer.innerHTML = toc;

const searchContainer = this.spec.doc.createElement('div');
searchContainer.setAttribute('id', 'menu-search');
searchContainer.innerHTML = '<input type=text id=menu-search-box placeholder=Search...><div id=menu-search-results class=inactive></div>';


const menuContainer = this.spec.doc.createElement('div');
menuContainer.setAttribute('id', 'menu');
menuContainer.appendChild(searchContainer);
menuContainer.appendChild(tocContainer);

this.spec.doc.body.insertBefore(menuContainer, this.spec.doc.body.firstChild);
Expand All @@ -21,5 +28,11 @@ module.exports = class Menu extends Builder {
menuToggle.textContent = '☰';

this.spec.doc.body.insertBefore(menuToggle, this.spec.doc.body.firstChild);

const biblioContainer = this.spec.doc.createElement('script');
biblioContainer.setAttribute('type', 'application/json');
biblioContainer.id = 'menu-search-biblio';
biblioContainer.textContent = JSON.stringify(this.spec.biblio);
this.spec.doc.head.appendChild(biblioContainer);
}
};
2 changes: 1 addition & 1 deletion test/test.html.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head><meta charset="utf-8">
<link rel="stylesheet" href="css/elements.css">
<script src="ecmarkup.js"></script>
<title>Ecmarkup Test Document</title></head><body><div id="menu-toggle">☰</div><div id="menu"><div id="menu-toc"><ol class="toc"><li><span class="item-toggle">◢</span><a href="#" title="Intro"><span class="secnum"></span> Intro</a><ol class="toc"><li><span class="item-toggle-none"></span><a href="#" title="Sub Intro"><span class="secnum"></span> Sub Intro</a></li></ol></li><li><span class="item-toggle">◢</span><a href="#" title="Clause Foo(_a_, _b_)"><span class="secnum">1</span> Clause Foo(<var>a</var>, <var>b</var>)</a><ol class="toc"><li><span class="item-toggle-none"></span><a href="#Foo" title="Sub Clause"><span class="secnum">1.1</span> Sub Clause</a></li><li><span class="item-toggle-none"></span><a href="#Bar" title="Sub Clause"><span class="secnum">1.2</span> Sub Clause</a></li><li><span class="item-toggle">◢</span><a href="#Baz" title="Header"><span class="secnum">1.3</span> Header</a><ol class="toc"><li><span class="item-toggle-none"></span><a href="#import3" title="Import 3"><span class="secnum">1.3.1</span> Import 3</a></li></ol></li></ol></li><li><span class="item-toggle-none"></span><a href="#" title="Annex"><span class="secnum">A</span> Annex</a></li></ol></div></div><h1 class="version">Draft 1 / September 26, 2015</h1><h1 class="title">Ecmarkup Test Document</h1>
<title>Ecmarkup Test Document</title><script type="application/json" id="menu-search-biblio">{"clauses":{"":{"location":"","id":"","aoid":null,"title":"Annex","number":"A"},"Foo":{"location":"","id":"Foo","aoid":"Foo","title":"Sub Clause","number":"1.1"},"Bar":{"location":"","id":"Bar","aoid":"Bar","title":"Sub Clause","number":"1.2"},"Baz":{"location":"","id":"Baz","aoid":"Baz","title":"Header","number":"1.3"},"import3":{"location":"","id":"import3","aoid":null,"title":"Import 3","number":"1.3.1"}},"ops":{"Foo":{"aoid":"Foo","id":"Foo","location":""},"Bar":{"aoid":"Bar","id":"Bar","location":""},"Baz":{"aoid":"Baz","id":"Baz","location":""}},"productions":{"prod-WhileStatement":{"id":"prod-WhileStatement","location":"","name":"WhileStatement"},"prod-ArgumentList":{"id":"prod-ArgumentList","location":"","name":"ArgumentList"},"prod-SourceCharacter":{"id":"prod-SourceCharacter","location":"","name":"SourceCharacter"},"prod-ExpressionStatement":{"id":"prod-ExpressionStatement","location":"","name":"ExpressionStatement"},"prod-StatementList":{"id":"prod-StatementList","location":"","name":"StatementList"},"prod-Identifier":{"id":"prod-Identifier","location":"","name":"Identifier"},"prod-EnumDeclaration":{"id":"prod-EnumDeclaration","location":"","name":"EnumDeclaration"},"prod-FooBar":{"id":"prod-FooBar","location":"","name":"FooBar"},"prod-FunctionDeclaration":{"id":"prod-FunctionDeclaration","location":"","name":"FunctionDeclaration"}},"terms":{},"examples":{},"notes":{},"tables":{},"figures":{}}</script></head><body><div id="menu-toggle">☰</div><div id="menu"><div id="menu-search"><input type="text" id="menu-search-box" placeholder="Search..."><div id="menu-search-results" class="inactive"></div></div><div id="menu-toc"><ol class="toc"><li><span class="item-toggle">◢</span><a href="#" title="Intro"><span class="secnum"></span> Intro</a><ol class="toc"><li><span class="item-toggle-none"></span><a href="#" title="Sub Intro"><span class="secnum"></span> Sub Intro</a></li></ol></li><li><span class="item-toggle">◢</span><a href="#" title="Clause Foo(_a_, _b_)"><span class="secnum">1</span> Clause Foo(<var>a</var>, <var>b</var>)</a><ol class="toc"><li><span class="item-toggle-none"></span><a href="#Foo" title="Sub Clause"><span class="secnum">1.1</span> Sub Clause</a></li><li><span class="item-toggle-none"></span><a href="#Bar" title="Sub Clause"><span class="secnum">1.2</span> Sub Clause</a></li><li><span class="item-toggle">◢</span><a href="#Baz" title="Header"><span class="secnum">1.3</span> Header</a><ol class="toc"><li><span class="item-toggle-none"></span><a href="#import3" title="Import 3"><span class="secnum">1.3.1</span> Import 3</a></li></ol></li></ol></li><li><span class="item-toggle-none"></span><a href="#" title="Annex"><span class="secnum">A</span> Annex</a></li></ol></div></div><h1 class="version">Draft 1 / September 26, 2015</h1><h1 class="title">Ecmarkup Test Document</h1>
<emu-intro>
<h1><span class="secnum"></span>Intro<span class="utils"><span class="anchor"><a href="#">#</a></span></span></h1>
<emu-intro>
Expand Down

0 comments on commit 03d1c14

Please sign in to comment.