Skip to content

Commit 03d1c14

Browse files
committed
Add quick search
1 parent d4ecf14 commit 03d1c14

File tree

4 files changed

+251
-3
lines changed

4 files changed

+251
-3
lines changed

css/elements.css

+31-1
Original file line numberDiff line numberDiff line change
@@ -472,14 +472,44 @@ del.block {
472472
color: #B6C8E4;
473473
}
474474

475+
#menu-search {
476+
color: #B6C8E4;
477+
}
478+
479+
#menu-search-box {
480+
display: block;
481+
width: 90%;
482+
margin: 5px auto;
483+
font-size: 1em;
484+
padding: 2px;
485+
}
486+
487+
#menu-search-results.inactive {
488+
display: none;
489+
}
490+
491+
#menu-search-results ul {
492+
list-style-type: square;
493+
padding: 0 0 0 35px;
494+
margin: 0;
495+
}
496+
497+
#menu-search-results li {
498+
white-space: nowrap;
499+
}
500+
501+
#menu-search-results a {
502+
color: #b6c8e4;
503+
}
504+
475505
@media (max-width: 1366px) {
476506
body {
477507
margin: 0 0 0 150px;
478508
}
479509

480510
#menu {
481511
display: none;
482-
padding-top: 2em;
512+
padding-top: 3em;
483513
width: 323px;
484514
}
485515

js/menu.js

+206-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,29 @@
33
function Menu() {
44
this.$toggle = document.getElementById('menu-toggle');
55
this.$menu = document.getElementById('menu');
6+
this.$searchBox = document.getElementById('menu-search-box');
7+
this.$searchResults = document.getElementById('menu-search-results');
8+
this.initSearch();
69

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

12+
this.$searchBox.addEventListener('keydown', function (e) {
13+
if (e.keyCode === 191 && e.target.value.length === 0) {
14+
e.preventDefault();
15+
e.stopPropagation();
16+
} else if (e.keyCode === 13) {
17+
e.preventDefault();
18+
e.stopPropagation();
19+
this.selectResult();
20+
}
21+
}.bind(this));
22+
23+
this.$searchBox.addEventListener('keyup', debounce(function (e) {
24+
e.stopPropagation();
25+
this.search(e.target.value);
26+
}.bind(this)));
27+
28+
929
var tocItems = this.$menu.querySelectorAll('#menu-toc li');
1030
for (var i = 0; i < tocItems.length; i++) {
1131
var $item = tocItems[i];
@@ -26,11 +46,196 @@ function Menu() {
2646
}
2747

2848
Menu.prototype.toggle = function () {
29-
this.$menu.classList.toggle("active");
49+
this.$menu.classList.toggle('active');
50+
}
51+
52+
Menu.prototype.show = function () {
53+
this.$menu.classList.add('active');
54+
}
55+
56+
Menu.prototype.hide = function () {
57+
this.$menu.classList.remove('active');
58+
}
59+
60+
Menu.prototype.isVisible = function() {
61+
return this.$menu.classList.contains('active');
62+
}
63+
64+
Menu.prototype.initSearch = function () {
65+
var $biblio = document.getElementById('menu-search-biblio');
66+
if (!$biblio) {
67+
this.biblio = {};
68+
} else {
69+
this.biblio = JSON.parse($biblio.textContent);
70+
}
71+
72+
document.addEventListener('keydown', function (e) {
73+
if (e.keyCode === 191) {
74+
e.preventDefault();
75+
e.stopPropagation();
76+
77+
if(this.isVisible()) {
78+
this._closeAfterSearch = false;
79+
} else {
80+
this._closeAfterSearch = true;
81+
this.show();
82+
}
83+
84+
this.show();
85+
this.$searchBox.focus();
86+
}
87+
}.bind(this))
88+
}
89+
90+
Menu.prototype.search = function (needle) {
91+
if (needle.length < 2) {
92+
this.hideSearch();
93+
} else {
94+
this.showSearch();
95+
}
96+
97+
needle = needle.toLowerCase();
98+
99+
var results = {};
100+
var seenClauses = {};
101+
102+
results.ops = Object.keys(this.biblio.ops).map(function (k) {
103+
return this.biblio.ops[k];
104+
}.bind(this)).filter(function(op) {
105+
return fuzzysearch(needle, op.aoid.toLowerCase());
106+
});
107+
108+
results.ops.forEach(function(op) {
109+
seenClauses[op.id] = true;
110+
});
111+
112+
results.productions = Object.keys(this.biblio.productions).map(function (k) {
113+
return this.biblio.productions[k];
114+
}.bind(this)).filter(function(prod) {
115+
return fuzzysearch(needle, prod.name.toLowerCase());
116+
});
117+
118+
results.clauses = Object.keys(this.biblio.clauses).map(function (k) {
119+
return this.biblio.clauses[k];
120+
}.bind(this)).filter(function(clause) {
121+
return !seenClauses[clause.id] && (clause.number.indexOf(needle) === 0 || fuzzysearch(needle, clause.title.toLowerCase()));
122+
});
123+
124+
if (results.length > 50) {
125+
results = results.slice(0, 50);
126+
}
127+
128+
this.displayResults(results);
129+
}
130+
131+
Menu.prototype.displayResults = function (results) {
132+
var totalResults = Object.keys(results).reduce(function (sum, record) { return sum + record.length }, 0);
133+
134+
if (totalResults > 0) {
135+
this.$searchResults.classList.remove('no-results');
136+
137+
var html = '<ul>';
138+
139+
results.ops.forEach(function (op) {
140+
html += '<li class=menu-search-result-op><a href="#' + op.id + '">' + op.aoid + '</a></li>'
141+
});
142+
143+
results.productions.forEach(function (prod) {
144+
html += '<li class=menu-search-result-prod><a href="#' + prod.id + '">' + prod.name + '</a></li>'
145+
});
146+
147+
results.clauses.forEach(function (clause) {
148+
html += '<li class=menu-search-result-clause><a href="#' + clause.id + '">' + clause.number + ' ' + clause.title + '</a></li>'
149+
})
150+
151+
html += '</ul>'
152+
153+
this.$searchResults.innerHTML = html;
154+
} else {
155+
this.$searchResults.classList.add('no-results');
156+
}
157+
}
158+
159+
Menu.prototype.hideSearch = function () {
160+
this.$searchResults.classList.add('inactive');
161+
}
162+
163+
Menu.prototype.showSearch = function () {
164+
this.$searchResults.classList.remove('inactive');
165+
}
166+
167+
Menu.prototype.selectResult = function () {
168+
var $first = this.$searchResults.querySelector('li:first-child a');
169+
170+
if ($first) {
171+
document.location = $first.getAttribute('href');
172+
}
173+
174+
this.$searchBox.value = '';
175+
this.$searchBox.blur();
176+
this.hideSearch();
177+
178+
if (this._closeAfterSearch) {
179+
this.hide();
180+
}
30181
}
31182

32183
function init() {
33184
var menu = new Menu();
34185
}
35186

36187
document.addEventListener('DOMContentLoaded', init);
188+
189+
function debounce(fn) {
190+
var timeout;
191+
return function() {
192+
var args = arguments;
193+
if (timeout) {
194+
clearTimeout(timeout);
195+
}
196+
timeout = setTimeout(function() {
197+
timeout = null;
198+
fn.apply(this, args);
199+
}.bind(this), 150);
200+
}
201+
}
202+
203+
// The following license applies to the fuzzysearch function
204+
// The MIT License (MIT)
205+
// Copyright © 2015 Nicolas Bevacqua
206+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
207+
// this software and associated documentation files (the "Software"), to deal in
208+
// the Software without restriction, including without limitation the rights to
209+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
210+
// the Software, and to permit persons to whom the Software is furnished to do so,
211+
// subject to the following conditions:
212+
213+
// The above copyright notice and this permission notice shall be included in all
214+
// copies or substantial portions of the Software.
215+
216+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
217+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
218+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
219+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
220+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
221+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
222+
function fuzzysearch (needle, haystack) {
223+
var tlen = haystack.length;
224+
var qlen = needle.length;
225+
if (qlen > tlen) {
226+
return false;
227+
}
228+
if (qlen === tlen) {
229+
return needle === haystack;
230+
}
231+
outer: for (var i = 0, j = 0; i < qlen; i++) {
232+
var nch = needle.charCodeAt(i);
233+
while (j < tlen) {
234+
if (haystack.charCodeAt(j++) === nch) {
235+
continue outer;
236+
}
237+
}
238+
return false;
239+
}
240+
return true;
241+
}

lib/Menu.js

+13
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ const Toc = require('./Toc');
66
module.exports = class Menu extends Builder {
77
build() {
88
const toc = Toc.build(this.spec, true);
9+
910
const tocContainer = this.spec.doc.createElement('div');
1011
tocContainer.setAttribute('id', 'menu-toc');
1112
tocContainer.innerHTML = toc;
1213

14+
const searchContainer = this.spec.doc.createElement('div');
15+
searchContainer.setAttribute('id', 'menu-search');
16+
searchContainer.innerHTML = '<input type=text id=menu-search-box placeholder=Search...><div id=menu-search-results class=inactive></div>';
17+
18+
1319
const menuContainer = this.spec.doc.createElement('div');
1420
menuContainer.setAttribute('id', 'menu');
21+
menuContainer.appendChild(searchContainer);
1522
menuContainer.appendChild(tocContainer);
1623

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

2330
this.spec.doc.body.insertBefore(menuToggle, this.spec.doc.body.firstChild);
31+
32+
const biblioContainer = this.spec.doc.createElement('script');
33+
biblioContainer.setAttribute('type', 'application/json');
34+
biblioContainer.id = 'menu-search-biblio';
35+
biblioContainer.textContent = JSON.stringify(this.spec.biblio);
36+
this.spec.doc.head.appendChild(biblioContainer);
2437
}
2538
};

test/test.html.baseline

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<head><meta charset="utf-8">
33
<link rel="stylesheet" href="css/elements.css">
44
<script src="ecmarkup.js"></script>
5-
<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>
5+
<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>
66
<emu-intro>
77
<h1><span class="secnum"></span>Intro<span class="utils"><span class="anchor"><a href="#">#</a></span></span></h1>
88
<emu-intro>

0 commit comments

Comments
 (0)