1 | "use strict";
|
---|
2 |
|
---|
3 | function Search(menu) {
|
---|
4 | this.menu = menu;
|
---|
5 | this.$search = document.getElementById('menu-search');
|
---|
6 | this.$searchBox = document.getElementById('menu-search-box');
|
---|
7 | this.$searchResults = document.getElementById('menu-search-results');
|
---|
8 |
|
---|
9 | this.loadBiblio();
|
---|
10 |
|
---|
11 | document.addEventListener('keydown', this.documentKeydown.bind(this));
|
---|
12 |
|
---|
13 | this.$searchBox.addEventListener('keydown', debounce(this.searchBoxKeydown.bind(this), { stopPropagation: true }));
|
---|
14 | this.$searchBox.addEventListener('keyup', debounce(this.searchBoxKeyup.bind(this), { stopPropagation: true }));
|
---|
15 | }
|
---|
16 |
|
---|
17 | Search.prototype.loadBiblio = function () {
|
---|
18 | var $biblio = document.getElementById('menu-search-biblio');
|
---|
19 | if (!$biblio) {
|
---|
20 | this.biblio = [];
|
---|
21 | } else {
|
---|
22 | this.biblio = JSON.parse($biblio.textContent);
|
---|
23 | this.biblio.clauses = this.biblio.filter(function (e) { return e.type === 'clause' });
|
---|
24 | this.biblio.byId = this.biblio.reduce(function (map, entry) {
|
---|
25 | map[entry.id] = entry;
|
---|
26 | return map;
|
---|
27 | }, {});
|
---|
28 | }
|
---|
29 | }
|
---|
30 |
|
---|
31 | Search.prototype.documentKeydown = function (e) {
|
---|
32 | if (e.keyCode === 191) {
|
---|
33 | e.preventDefault();
|
---|
34 | e.stopPropagation();
|
---|
35 | this.triggerSearch();
|
---|
36 | }
|
---|
37 | }
|
---|
38 |
|
---|
39 | Search.prototype.searchBoxKeydown = function (e) {
|
---|
40 | e.stopPropagation();
|
---|
41 | e.preventDefault();
|
---|
42 | if (e.keyCode === 191 && e.target.value.length === 0) {
|
---|
43 | e.preventDefault();
|
---|
44 | } else if (e.keyCode === 13) {
|
---|
45 | e.preventDefault();
|
---|
46 | this.selectResult();
|
---|
47 | }
|
---|
48 | }
|
---|
49 |
|
---|
50 | Search.prototype.searchBoxKeyup = function (e) {
|
---|
51 | if (e.keyCode === 13 || e.keyCode === 9) {
|
---|
52 | return;
|
---|
53 | }
|
---|
54 |
|
---|
55 | this.search(e.target.value);
|
---|
56 | }
|
---|
57 |
|
---|
58 |
|
---|
59 | Search.prototype.triggerSearch = function (e) {
|
---|
60 | if (this.menu.isVisible()) {
|
---|
61 | this._closeAfterSearch = false;
|
---|
62 | } else {
|
---|
63 | this._closeAfterSearch = true;
|
---|
64 | this.menu.show();
|
---|
65 | }
|
---|
66 |
|
---|
67 | this.$searchBox.focus();
|
---|
68 | this.$searchBox.select();
|
---|
69 | }
|
---|
70 | // bit 12 - Set if the result starts with searchString
|
---|
71 | // bits 8-11: 8 - number of chunks multiplied by 2 if cases match, otherwise 1.
|
---|
72 | // bits 1-7: 127 - length of the entry
|
---|
73 | // General scheme: prefer case sensitive matches with fewer chunks, and otherwise
|
---|
74 | // prefer shorter matches.
|
---|
75 | function relevance(result, searchString) {
|
---|
76 | var relevance = 0;
|
---|
77 |
|
---|
78 | relevance = Math.max(0, 8 - result.match.chunks) << 7;
|
---|
79 |
|
---|
80 | if (result.match.caseMatch) {
|
---|
81 | relevance *= 2;
|
---|
82 | }
|
---|
83 |
|
---|
84 | if (result.match.prefix) {
|
---|
85 | relevance += 2048
|
---|
86 | }
|
---|
87 |
|
---|
88 | relevance += Math.max(0, 255 - result.entry.key.length);
|
---|
89 |
|
---|
90 | return relevance;
|
---|
91 | }
|
---|
92 |
|
---|
93 | Search.prototype.search = function (searchString) {
|
---|
94 | var s = Date.now();
|
---|
95 |
|
---|
96 | if (searchString === '') {
|
---|
97 | this.displayResults([]);
|
---|
98 | this.hideSearch();
|
---|
99 | return;
|
---|
100 | } else {
|
---|
101 | this.showSearch();
|
---|
102 | }
|
---|
103 |
|
---|
104 | if (searchString.length === 1) {
|
---|
105 | this.displayResults([]);
|
---|
106 | return;
|
---|
107 | }
|
---|
108 |
|
---|
109 | var results;
|
---|
110 |
|
---|
111 | if (/^[\d\.]*$/.test(searchString)) {
|
---|
112 | results = this.biblio.clauses.filter(function (clause) {
|
---|
113 | return clause.number.substring(0, searchString.length) === searchString;
|
---|
114 | }).map(function (clause) {
|
---|
115 | return { entry: clause };
|
---|
116 | });
|
---|
117 | } else {
|
---|
118 | results = [];
|
---|
119 |
|
---|
120 | for (var i = 0; i < this.biblio.length; i++) {
|
---|
121 | var entry = this.biblio[i];
|
---|
122 | if (!entry.key) {
|
---|
123 | // biblio entries without a key aren't searchable
|
---|
124 | continue;
|
---|
125 | }
|
---|
126 |
|
---|
127 | var match = fuzzysearch(searchString, entry.key);
|
---|
128 | if (match) {
|
---|
129 | results.push({ entry: entry, match: match });
|
---|
130 | }
|
---|
131 | }
|
---|
132 |
|
---|
133 | results.forEach(function (result) {
|
---|
134 | result.relevance = relevance(result, searchString);
|
---|
135 | });
|
---|
136 |
|
---|
137 | results = results.sort(function (a, b) { return b.relevance - a.relevance });
|
---|
138 |
|
---|
139 | }
|
---|
140 |
|
---|
141 | if (results.length > 50) {
|
---|
142 | results = results.slice(0, 50);
|
---|
143 | }
|
---|
144 |
|
---|
145 | this.displayResults(results);
|
---|
146 | }
|
---|
147 | Search.prototype.hideSearch = function () {
|
---|
148 | this.$search.classList.remove('active');
|
---|
149 | }
|
---|
150 |
|
---|
151 | Search.prototype.showSearch = function () {
|
---|
152 | this.$search.classList.add('active');
|
---|
153 | }
|
---|
154 |
|
---|
155 | Search.prototype.selectResult = function () {
|
---|
156 | var $first = this.$searchResults.querySelector('li:first-child a');
|
---|
157 |
|
---|
158 | if ($first) {
|
---|
159 | document.location = $first.getAttribute('href');
|
---|
160 | }
|
---|
161 |
|
---|
162 | this.$searchBox.value = '';
|
---|
163 | this.$searchBox.blur();
|
---|
164 | this.displayResults([]);
|
---|
165 | this.hideSearch();
|
---|
166 |
|
---|
167 | if (this._closeAfterSearch) {
|
---|
168 | this.menu.hide();
|
---|
169 | }
|
---|
170 | }
|
---|
171 |
|
---|
172 | Search.prototype.displayResults = function (results) {
|
---|
173 | if (results.length > 0) {
|
---|
174 | this.$searchResults.classList.remove('no-results');
|
---|
175 |
|
---|
176 | var html = '<ul>';
|
---|
177 |
|
---|
178 | results.forEach(function (result) {
|
---|
179 | var entry = result.entry;
|
---|
180 | var id = entry.id;
|
---|
181 | var cssClass = '';
|
---|
182 | var text = '';
|
---|
183 |
|
---|
184 | if (entry.type === 'clause') {
|
---|
185 | var number = entry.number ? entry.number + ' ' : '';
|
---|
186 | text = number + entry.key;
|
---|
187 | cssClass = 'clause';
|
---|
188 | id = entry.id;
|
---|
189 | } else if (entry.type === 'production') {
|
---|
190 | text = entry.key;
|
---|
191 | cssClass = 'prod';
|
---|
192 | id = entry.id;
|
---|
193 | } else if (entry.type === 'op') {
|
---|
194 | text = entry.key;
|
---|
195 | cssClass = 'op';
|
---|
196 | id = entry.id || entry.refId;
|
---|
197 | } else if (entry.type === 'term') {
|
---|
198 | text = entry.key;
|
---|
199 | cssClass = 'term';
|
---|
200 | id = entry.id || entry.refId;
|
---|
201 | }
|
---|
202 |
|
---|
203 | if (text) {
|
---|
204 | html += '<li class=menu-search-result-' + cssClass + '><a href="#' + id + '">' + text + '</a></li>'
|
---|
205 | }
|
---|
206 | });
|
---|
207 |
|
---|
208 | html += '</ul>'
|
---|
209 |
|
---|
210 | this.$searchResults.innerHTML = html;
|
---|
211 | } else {
|
---|
212 | this.$searchResults.innerHTML = '';
|
---|
213 | this.$searchResults.classList.add('no-results');
|
---|
214 | }
|
---|
215 | }
|
---|
216 |
|
---|
217 |
|
---|
218 | function Menu() {
|
---|
219 | this.$toggle = document.getElementById('menu-toggle');
|
---|
220 | this.$menu = document.getElementById('menu');
|
---|
221 | this.$toc = document.querySelector('menu-toc > ol');
|
---|
222 | this.$pins = document.querySelector('#menu-pins');
|
---|
223 | this.$pinList = document.getElementById('menu-pins-list');
|
---|
224 | this.$toc = document.querySelector('#menu-toc > ol');
|
---|
225 | this.$specContainer = document.getElementById('spec-container');
|
---|
226 | this.search = new Search(this);
|
---|
227 |
|
---|
228 | this._pinnedIds = {};
|
---|
229 | this.loadPinEntries();
|
---|
230 |
|
---|
231 | // toggle menu
|
---|
232 | this.$toggle.addEventListener('click', this.toggle.bind(this));
|
---|
233 |
|
---|
234 | // keydown events for pinned clauses
|
---|
235 | document.addEventListener('keydown', this.documentKeydown.bind(this));
|
---|
236 |
|
---|
237 | // toc expansion
|
---|
238 | var tocItems = this.$menu.querySelectorAll('#menu-toc li');
|
---|
239 | for (var i = 0; i < tocItems.length; i++) {
|
---|
240 | var $item = tocItems[i];
|
---|
241 | $item.addEventListener('click', function($item, event) {
|
---|
242 | $item.classList.toggle('active');
|
---|
243 | event.stopPropagation();
|
---|
244 | }.bind(null, $item));
|
---|
245 | }
|
---|
246 |
|
---|
247 | // close toc on toc item selection
|
---|
248 | var tocLinks = this.$menu.querySelectorAll('#menu-toc li > a');
|
---|
249 | for (var i = 0; i < tocLinks.length; i++) {
|
---|
250 | var $link = tocLinks[i];
|
---|
251 | $link.addEventListener('click', function(event) {
|
---|
252 | this.toggle();
|
---|
253 | event.stopPropagation();
|
---|
254 | }.bind(this));
|
---|
255 | }
|
---|
256 |
|
---|
257 | // update active clause on scroll
|
---|
258 | window.addEventListener('scroll', debounce(this.updateActiveClause.bind(this)));
|
---|
259 | this.updateActiveClause();
|
---|
260 |
|
---|
261 | // prevent menu scrolling from scrolling the body
|
---|
262 | this.$toc.addEventListener('wheel', function (e) {
|
---|
263 | var target = e.currentTarget;
|
---|
264 | var offTop = e.deltaY < 0 && target.scrollTop === 0;
|
---|
265 | if (offTop) {
|
---|
266 | e.preventDefault();
|
---|
267 | }
|
---|
268 | var offBottom = e.deltaY > 0
|
---|
269 | && target.offsetHeight + target.scrollTop >= target.scrollHeight;
|
---|
270 |
|
---|
271 | if (offBottom) {
|
---|
272 | e.preventDefault();
|
---|
273 | }
|
---|
274 | })
|
---|
275 | }
|
---|
276 |
|
---|
277 | Menu.prototype.documentKeydown = function (e) {
|
---|
278 | e.stopPropagation();
|
---|
279 | if (e.keyCode === 80) {
|
---|
280 | this.togglePinEntry();
|
---|
281 | } else if (e.keyCode > 48 && e.keyCode < 58) {
|
---|
282 | this.selectPin(e.keyCode - 49);
|
---|
283 | }
|
---|
284 | }
|
---|
285 |
|
---|
286 | Menu.prototype.updateActiveClause = function () {
|
---|
287 | this.setActiveClause(findActiveClause(this.$specContainer))
|
---|
288 | }
|
---|
289 |
|
---|
290 | Menu.prototype.setActiveClause = function (clause) {
|
---|
291 | this.$activeClause = clause;
|
---|
292 | this.revealInToc(this.$activeClause);
|
---|
293 | }
|
---|
294 |
|
---|
295 | Menu.prototype.revealInToc = function (path) {
|
---|
296 | var current = this.$toc.querySelectorAll('li.revealed');
|
---|
297 | for (var i = 0; i < current.length; i++) {
|
---|
298 | current[i].classList.remove('revealed');
|
---|
299 | current[i].classList.remove('revealed-leaf');
|
---|
300 | }
|
---|
301 |
|
---|
302 | var current = this.$toc;
|
---|
303 | var index = 0;
|
---|
304 | while (index < path.length) {
|
---|
305 | var children = current.children;
|
---|
306 | for (var i = 0; i < children.length; i++) {
|
---|
307 | if ('#' + path[index].id === children[i].children[1].getAttribute('href') ) {
|
---|
308 | children[i].classList.add('revealed');
|
---|
309 | if (index === path.length - 1) {
|
---|
310 | children[i].classList.add('revealed-leaf');
|
---|
311 | var rect = children[i].getBoundingClientRect();
|
---|
312 | this.$toc.getBoundingClientRect().top
|
---|
313 | var tocRect = this.$toc.getBoundingClientRect();
|
---|
314 | if (rect.top + 10 > tocRect.bottom) {
|
---|
315 | this.$toc.scrollTop = this.$toc.scrollTop + (rect.top - tocRect.bottom) + (rect.bottom - rect.top);
|
---|
316 | } else if (rect.top < tocRect.top) {
|
---|
317 | this.$toc.scrollTop = this.$toc.scrollTop - (tocRect.top - rect.top);
|
---|
318 | }
|
---|
319 | }
|
---|
320 | current = children[i].querySelector('ol');
|
---|
321 | index++;
|
---|
322 | break;
|
---|
323 | }
|
---|
324 | }
|
---|
325 |
|
---|
326 | }
|
---|
327 | }
|
---|
328 |
|
---|
329 | function findActiveClause(root, path) {
|
---|
330 | var clauses = new ClauseWalker(root);
|
---|
331 | var $clause;
|
---|
332 | var found = false;
|
---|
333 | var path = path || [];
|
---|
334 |
|
---|
335 | while ($clause = clauses.nextNode()) {
|
---|
336 | var rect = $clause.getBoundingClientRect();
|
---|
337 | var $header = $clause.children[0];
|
---|
338 | var marginTop = parseInt(getComputedStyle($header)["margin-top"]);
|
---|
339 |
|
---|
340 | if ((rect.top - marginTop) <= 0 && rect.bottom > 0) {
|
---|
341 | found = true;
|
---|
342 | return findActiveClause($clause, path.concat($clause)) || path;
|
---|
343 | }
|
---|
344 | }
|
---|
345 |
|
---|
346 | return path;
|
---|
347 | }
|
---|
348 |
|
---|
349 | function ClauseWalker(root) {
|
---|
350 | var previous;
|
---|
351 | var treeWalker = document.createTreeWalker(
|
---|
352 | root,
|
---|
353 | NodeFilter.SHOW_ELEMENT,
|
---|
354 | {
|
---|
355 | acceptNode: function (node) {
|
---|
356 | if (previous === node.parentNode) {
|
---|
357 | return NodeFilter.FILTER_REJECT;
|
---|
358 | } else {
|
---|
359 | previous = node;
|
---|
360 | }
|
---|
361 | if (node.nodeName === 'EMU-CLAUSE' || node.nodeName === 'EMU-INTRO' || node.nodeName === 'EMU-ANNEX') {
|
---|
362 | return NodeFilter.FILTER_ACCEPT;
|
---|
363 | } else {
|
---|
364 | return NodeFilter.FILTER_SKIP;
|
---|
365 | }
|
---|
366 | }
|
---|
367 | },
|
---|
368 | false
|
---|
369 | );
|
---|
370 |
|
---|
371 | return treeWalker;
|
---|
372 | }
|
---|
373 |
|
---|
374 | Menu.prototype.toggle = function () {
|
---|
375 | this.$menu.classList.toggle('active');
|
---|
376 | }
|
---|
377 |
|
---|
378 | Menu.prototype.show = function () {
|
---|
379 | this.$menu.classList.add('active');
|
---|
380 | }
|
---|
381 |
|
---|
382 | Menu.prototype.hide = function () {
|
---|
383 | this.$menu.classList.remove('active');
|
---|
384 | }
|
---|
385 |
|
---|
386 | Menu.prototype.isVisible = function() {
|
---|
387 | return this.$menu.classList.contains('active');
|
---|
388 | }
|
---|
389 |
|
---|
390 | Menu.prototype.showPins = function () {
|
---|
391 | this.$pins.classList.add('active');
|
---|
392 | }
|
---|
393 |
|
---|
394 | Menu.prototype.hidePins = function () {
|
---|
395 | this.$pins.classList.remove('active');
|
---|
396 | }
|
---|
397 |
|
---|
398 | Menu.prototype.addPinEntry = function (id) {
|
---|
399 | var entry = this.search.biblio.byId[id];
|
---|
400 | if (!entry) {
|
---|
401 | // id was deleted after pin (or something) so remove it
|
---|
402 | delete this._pinnedIds[id];
|
---|
403 | this.persistPinEntries();
|
---|
404 | return;
|
---|
405 | }
|
---|
406 |
|
---|
407 | if (entry.type === 'clause') {
|
---|
408 | var prefix;
|
---|
409 | if (entry.number) {
|
---|
410 | prefix = entry.number + ' ';
|
---|
411 | } else {
|
---|
412 | prefix = '';
|
---|
413 | }
|
---|
414 | this.$pinList.innerHTML += '<li><a href="#' + entry.id + '">' + prefix + entry.titleHTML + '</a></li>';
|
---|
415 | } else {
|
---|
416 | this.$pinList.innerHTML += '<li><a href="#' + entry.id + '">' + entry.key + '</a></li>';
|
---|
417 | }
|
---|
418 |
|
---|
419 | if (Object.keys(this._pinnedIds).length === 0) {
|
---|
420 | this.showPins();
|
---|
421 | }
|
---|
422 | this._pinnedIds[id] = true;
|
---|
423 | this.persistPinEntries();
|
---|
424 | }
|
---|
425 |
|
---|
426 | Menu.prototype.removePinEntry = function (id) {
|
---|
427 | var item = this.$pinList.querySelector('a[href="#' + id + '"]').parentNode;
|
---|
428 | this.$pinList.removeChild(item);
|
---|
429 | delete this._pinnedIds[id];
|
---|
430 | if (Object.keys(this._pinnedIds).length === 0) {
|
---|
431 | this.hidePins();
|
---|
432 | }
|
---|
433 |
|
---|
434 | this.persistPinEntries();
|
---|
435 | }
|
---|
436 |
|
---|
437 | Menu.prototype.persistPinEntries = function () {
|
---|
438 | try {
|
---|
439 | if (!window.localStorage) return;
|
---|
440 | } catch (e) {
|
---|
441 | return;
|
---|
442 | }
|
---|
443 |
|
---|
444 | localStorage.pinEntries = JSON.stringify(Object.keys(this._pinnedIds));
|
---|
445 | }
|
---|
446 |
|
---|
447 | Menu.prototype.loadPinEntries = function () {
|
---|
448 | try {
|
---|
449 | if (!window.localStorage) return;
|
---|
450 | } catch (e) {
|
---|
451 | return;
|
---|
452 | }
|
---|
453 |
|
---|
454 | var pinsString = window.localStorage.pinEntries;
|
---|
455 | if (!pinsString) return;
|
---|
456 | var pins = JSON.parse(pinsString);
|
---|
457 | for(var i = 0; i < pins.length; i++) {
|
---|
458 | this.addPinEntry(pins[i]);
|
---|
459 | }
|
---|
460 | }
|
---|
461 |
|
---|
462 | Menu.prototype.togglePinEntry = function (id) {
|
---|
463 | if (!id) {
|
---|
464 | id = this.$activeClause[this.$activeClause.length - 1].id;
|
---|
465 | }
|
---|
466 |
|
---|
467 | if (this._pinnedIds[id]) {
|
---|
468 | this.removePinEntry(id);
|
---|
469 | } else {
|
---|
470 | this.addPinEntry(id);
|
---|
471 | }
|
---|
472 | }
|
---|
473 |
|
---|
474 | Menu.prototype.selectPin = function (num) {
|
---|
475 | document.location = this.$pinList.children[num].children[0].href;
|
---|
476 | }
|
---|
477 |
|
---|
478 | var menu;
|
---|
479 | function init() {
|
---|
480 | menu = new Menu();
|
---|
481 | var $container = document.getElementById('spec-container');
|
---|
482 | $container.addEventListener('mouseover', debounce(function (e) {
|
---|
483 | Toolbox.activateIfMouseOver(e);
|
---|
484 | }));
|
---|
485 | }
|
---|
486 |
|
---|
487 | document.addEventListener('DOMContentLoaded', init);
|
---|
488 |
|
---|
489 | function debounce(fn, opts) {
|
---|
490 | opts = opts || {};
|
---|
491 | var timeout;
|
---|
492 | return function(e) {
|
---|
493 | if (opts.stopPropagation) {
|
---|
494 | e.stopPropagation();
|
---|
495 | }
|
---|
496 | var args = arguments;
|
---|
497 | if (timeout) {
|
---|
498 | clearTimeout(timeout);
|
---|
499 | }
|
---|
500 | timeout = setTimeout(function() {
|
---|
501 | timeout = null;
|
---|
502 | fn.apply(this, args);
|
---|
503 | }.bind(this), 150);
|
---|
504 | }
|
---|
505 | }
|
---|
506 |
|
---|
507 | var CLAUSE_NODES = ['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'];
|
---|
508 | function findLocalReferences ($elem) {
|
---|
509 | var name = $elem.innerHTML;
|
---|
510 | var references = [];
|
---|
511 |
|
---|
512 | var parentClause = $elem.parentNode;
|
---|
513 | while (parentClause && CLAUSE_NODES.indexOf(parentClause.nodeName) === -1) {
|
---|
514 | parentClause = parentClause.parentNode;
|
---|
515 | }
|
---|
516 |
|
---|
517 | if(!parentClause) return;
|
---|
518 |
|
---|
519 | var vars = parentClause.querySelectorAll('var');
|
---|
520 |
|
---|
521 | for (var i = 0; i < vars.length; i++) {
|
---|
522 | var $var = vars[i];
|
---|
523 |
|
---|
524 | if ($var.innerHTML === name) {
|
---|
525 | references.push($var);
|
---|
526 | }
|
---|
527 | }
|
---|
528 |
|
---|
529 | return references;
|
---|
530 | }
|
---|
531 |
|
---|
532 | function toggleFindLocalReferences($elem) {
|
---|
533 | var references = findLocalReferences($elem);
|
---|
534 | if ($elem.classList.contains('referenced')) {
|
---|
535 | references.forEach(function ($reference) {
|
---|
536 | $reference.classList.remove('referenced');
|
---|
537 | });
|
---|
538 | } else {
|
---|
539 | references.forEach(function ($reference) {
|
---|
540 | $reference.classList.add('referenced');
|
---|
541 | });
|
---|
542 | }
|
---|
543 | }
|
---|
544 |
|
---|
545 | function installFindLocalReferences () {
|
---|
546 | document.addEventListener('click', function (e) {
|
---|
547 | if (e.target.nodeName === 'VAR') {
|
---|
548 | toggleFindLocalReferences(e.target);
|
---|
549 | }
|
---|
550 | });
|
---|
551 | }
|
---|
552 |
|
---|
553 | document.addEventListener('DOMContentLoaded', installFindLocalReferences);
|
---|
554 |
|
---|
555 |
|
---|
556 |
|
---|
557 |
|
---|
558 | // The following license applies to the fuzzysearch function
|
---|
559 | // The MIT License (MIT)
|
---|
560 | // Copyright © 2015 Nicolas Bevacqua
|
---|
561 | // Copyright © 2016 Brian Terlson
|
---|
562 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
|
---|
563 | // this software and associated documentation files (the "Software"), to deal in
|
---|
564 | // the Software without restriction, including without limitation the rights to
|
---|
565 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
---|
566 | // the Software, and to permit persons to whom the Software is furnished to do so,
|
---|
567 | // subject to the following conditions:
|
---|
568 |
|
---|
569 | // The above copyright notice and this permission notice shall be included in all
|
---|
570 | // copies or substantial portions of the Software.
|
---|
571 |
|
---|
572 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
---|
573 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
---|
574 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
---|
575 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
---|
576 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
---|
577 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
---|
578 | function fuzzysearch (searchString, haystack, caseInsensitive) {
|
---|
579 | var tlen = haystack.length;
|
---|
580 | var qlen = searchString.length;
|
---|
581 | var chunks = 1;
|
---|
582 | var finding = false;
|
---|
583 | var prefix = true;
|
---|
584 |
|
---|
585 | if (qlen > tlen) {
|
---|
586 | return false;
|
---|
587 | }
|
---|
588 |
|
---|
589 | if (qlen === tlen) {
|
---|
590 | if (searchString === haystack) {
|
---|
591 | return { caseMatch: true, chunks: 1, prefix: true };
|
---|
592 | } else if (searchString.toLowerCase() === haystack.toLowerCase()) {
|
---|
593 | return { caseMatch: false, chunks: 1, prefix: true };
|
---|
594 | } else {
|
---|
595 | return false;
|
---|
596 | }
|
---|
597 | }
|
---|
598 |
|
---|
599 | outer: for (var i = 0, j = 0; i < qlen; i++) {
|
---|
600 | var nch = searchString[i];
|
---|
601 | while (j < tlen) {
|
---|
602 | var targetChar = haystack[j++];
|
---|
603 | if (targetChar === nch) {
|
---|
604 | finding = true;
|
---|
605 | continue outer;
|
---|
606 | }
|
---|
607 | if (finding) {
|
---|
608 | chunks++;
|
---|
609 | finding = false;
|
---|
610 | }
|
---|
611 | }
|
---|
612 |
|
---|
613 | if (caseInsensitive) { return false }
|
---|
614 |
|
---|
615 | return fuzzysearch(searchString.toLowerCase(), haystack.toLowerCase(), true);
|
---|
616 | }
|
---|
617 |
|
---|
618 | return { caseMatch: !caseInsensitive, chunks: chunks, prefix: j <= qlen };
|
---|
619 | }
|
---|
620 |
|
---|
621 | var Toolbox = {
|
---|
622 | init: function () {
|
---|
623 | this.$container = document.createElement('div');
|
---|
624 | this.$container.classList.add('toolbox');
|
---|
625 | this.$permalink = document.createElement('a');
|
---|
626 | this.$permalink.textContent = 'Permalink';
|
---|
627 | this.$pinLink = document.createElement('a');
|
---|
628 | this.$pinLink.textContent = 'Pin';
|
---|
629 | this.$pinLink.setAttribute('href', '#');
|
---|
630 | this.$pinLink.addEventListener('click', function (e) {
|
---|
631 | e.preventDefault();
|
---|
632 | e.stopPropagation();
|
---|
633 | menu.togglePinEntry(this.entry.id);
|
---|
634 | }.bind(this));
|
---|
635 |
|
---|
636 | this.$refsLink = document.createElement('a');
|
---|
637 | this.$refsLink.setAttribute('href', '#');
|
---|
638 | this.$refsLink.addEventListener('click', function (e) {
|
---|
639 | e.preventDefault();
|
---|
640 | e.stopPropagation();
|
---|
641 | referencePane.showReferencesFor(this.entry);
|
---|
642 | }.bind(this));
|
---|
643 | this.$container.appendChild(this.$permalink);
|
---|
644 | this.$container.appendChild(this.$pinLink);
|
---|
645 | this.$container.appendChild(this.$refsLink);
|
---|
646 | document.body.appendChild(this.$container);
|
---|
647 | },
|
---|
648 |
|
---|
649 | activate: function (el, entry, target) {
|
---|
650 | if (el === this._activeEl) return;
|
---|
651 | this.active = true;
|
---|
652 | this.entry = entry;
|
---|
653 | this.$container.classList.add('active');
|
---|
654 | this.top = el.offsetTop - this.$container.offsetHeight - 10;
|
---|
655 | this.left = el.offsetLeft;
|
---|
656 | this.$container.setAttribute('style', 'left: ' + this.left + 'px; top: ' + this.top + 'px');
|
---|
657 | this.updatePermalink();
|
---|
658 | this.updateReferences();
|
---|
659 | this._activeEl = el;
|
---|
660 | if (this.top < document.body.scrollTop && el === target) {
|
---|
661 | // don't scroll unless it's a small thing (< 200px)
|
---|
662 | this.$container.scrollIntoView();
|
---|
663 | }
|
---|
664 | },
|
---|
665 |
|
---|
666 | updatePermalink: function () {
|
---|
667 | this.$permalink.setAttribute('href', '#' + this.entry.id);
|
---|
668 | },
|
---|
669 |
|
---|
670 | updateReferences: function () {
|
---|
671 | this.$refsLink.textContent = 'References (' + this.entry.referencingIds.length + ')';
|
---|
672 | },
|
---|
673 |
|
---|
674 | activateIfMouseOver: function (e) {
|
---|
675 | var ref = this.findReferenceUnder(e.target);
|
---|
676 | if (ref && (!this.active || e.pageY > this._activeEl.offsetTop)) {
|
---|
677 | var entry = menu.search.biblio.byId[ref.id];
|
---|
678 | this.activate(ref.element, entry, e.target);
|
---|
679 | } else if (this.active && ((e.pageY < this.top) || e.pageY > (this._activeEl.offsetTop + this._activeEl.offsetHeight))) {
|
---|
680 | this.deactivate();
|
---|
681 | }
|
---|
682 | },
|
---|
683 |
|
---|
684 | findReferenceUnder: function (el) {
|
---|
685 | while (el) {
|
---|
686 | var parent = el.parentNode;
|
---|
687 | if (el.nodeName === 'H1' && parent.nodeName.match(/EMU-CLAUSE|EMU-ANNEX|EMU-INTRO/) && parent.id) {
|
---|
688 | return { element: el, id: parent.id };
|
---|
689 | } else if (el.nodeName.match(/EMU-(?!CLAUSE|XREF|ANNEX|INTRO)|DFN/) &&
|
---|
690 | el.id && el.id[0] !== '_') {
|
---|
691 | if (el.nodeName === 'EMU-FIGURE' || el.nodeName === 'EMU-TABLE' || el.nodeName === 'EMU-EXAMPLE') {
|
---|
692 | // return the figcaption element
|
---|
693 | return { element: el.children[0].children[0], id: el.id };
|
---|
694 | } else if (el.nodeName === 'EMU-PRODUCTION') {
|
---|
695 | // return the LHS non-terminal element
|
---|
696 | return { element: el.children[0], id: el.id };
|
---|
697 | } else {
|
---|
698 | return { element: el, id: el.id };
|
---|
699 | }
|
---|
700 | }
|
---|
701 | el = parent;
|
---|
702 | }
|
---|
703 | },
|
---|
704 |
|
---|
705 | deactivate: function () {
|
---|
706 | this.$container.classList.remove('active');
|
---|
707 | this._activeEl = null;
|
---|
708 | this.activeElBounds = null;
|
---|
709 | this.active = false;
|
---|
710 | }
|
---|
711 | }
|
---|
712 |
|
---|
713 | var referencePane = {
|
---|
714 | init: function() {
|
---|
715 | this.$container = document.createElement('div');
|
---|
716 | this.$container.setAttribute('id', 'references-pane-container');
|
---|
717 |
|
---|
718 | var $spacer = document.createElement('div');
|
---|
719 | $spacer.setAttribute('id', 'references-pane-spacer');
|
---|
720 |
|
---|
721 | this.$pane = document.createElement('div');
|
---|
722 | this.$pane.setAttribute('id', 'references-pane');
|
---|
723 |
|
---|
724 | this.$container.appendChild($spacer);
|
---|
725 | this.$container.appendChild(this.$pane);
|
---|
726 |
|
---|
727 | this.$header = document.createElement('div');
|
---|
728 | this.$header.classList.add('menu-pane-header');
|
---|
729 | this.$header.textContent = 'References to ';
|
---|
730 | this.$headerRefId = document.createElement('a');
|
---|
731 | this.$header.appendChild(this.$headerRefId);
|
---|
732 | this.$closeButton = document.createElement('span');
|
---|
733 | this.$closeButton.setAttribute('id', 'references-pane-close');
|
---|
734 | this.$closeButton.addEventListener('click', function (e) {
|
---|
735 | this.deactivate();
|
---|
736 | }.bind(this));
|
---|
737 | this.$header.appendChild(this.$closeButton);
|
---|
738 |
|
---|
739 | this.$pane.appendChild(this.$header);
|
---|
740 | var tableContainer = document.createElement('div');
|
---|
741 | tableContainer.setAttribute('id', 'references-pane-table-container');
|
---|
742 |
|
---|
743 | this.$table = document.createElement('table');
|
---|
744 | this.$table.setAttribute('id', 'references-pane-table');
|
---|
745 |
|
---|
746 | this.$tableBody = this.$table.createTBody();
|
---|
747 |
|
---|
748 | tableContainer.appendChild(this.$table);
|
---|
749 | this.$pane.appendChild(tableContainer);
|
---|
750 |
|
---|
751 | menu.$specContainer.appendChild(this.$container);
|
---|
752 | },
|
---|
753 |
|
---|
754 | activate: function () {
|
---|
755 | this.$container.classList.add('active');
|
---|
756 | },
|
---|
757 |
|
---|
758 | deactivate: function () {
|
---|
759 | this.$container.classList.remove('active');
|
---|
760 | },
|
---|
761 |
|
---|
762 | showReferencesFor(entry) {
|
---|
763 | this.activate();
|
---|
764 | var newBody = document.createElement('tbody');
|
---|
765 | var previousId;
|
---|
766 | var previousCell;
|
---|
767 | var dupCount = 0;
|
---|
768 | this.$headerRefId.textContent = '#' + entry.id;
|
---|
769 | this.$headerRefId.setAttribute('href', '#' + entry.id);
|
---|
770 | entry.referencingIds.map(function (id) {
|
---|
771 | var target = document.getElementById(id);
|
---|
772 | var cid = findParentClauseId(target);
|
---|
773 | var clause = menu.search.biblio.byId[cid];
|
---|
774 | var dupCount = 0;
|
---|
775 | return { id: id, clause: clause }
|
---|
776 | }).sort(function (a, b) {
|
---|
777 | return sortByClauseNumber(a.clause, b.clause);
|
---|
778 | }).forEach(function (record, i) {
|
---|
779 | if (previousId === record.clause.id) {
|
---|
780 | previousCell.innerHTML += ' (<a href="#' + record.id + '">' + (dupCount + 2) + '</a>)';
|
---|
781 | dupCount++;
|
---|
782 | } else {
|
---|
783 | var row = newBody.insertRow();
|
---|
784 | var cell = row.insertCell();
|
---|
785 | cell.innerHTML = record.clause.number;
|
---|
786 | cell = row.insertCell();
|
---|
787 | cell.innerHTML = '<a href="#' + record.id + '">' + record.clause.titleHTML + '</a>';
|
---|
788 | previousCell = cell;
|
---|
789 | previousId = record.clause.id;
|
---|
790 | dupCount = 0;
|
---|
791 | }
|
---|
792 | }, this);
|
---|
793 | this.$table.removeChild(this.$tableBody);
|
---|
794 | this.$tableBody = newBody;
|
---|
795 | this.$table.appendChild(this.$tableBody);
|
---|
796 | }
|
---|
797 | }
|
---|
798 | function findParentClauseId(node) {
|
---|
799 | while (node && node.nodeName !== 'EMU-CLAUSE' && node.nodeName !== 'EMU-INTRO' && node.nodeName !== 'EMU-ANNEX') {
|
---|
800 | node = node.parentNode;
|
---|
801 | }
|
---|
802 | if (!node) return null;
|
---|
803 | return node.getAttribute('id');
|
---|
804 | }
|
---|
805 |
|
---|
806 | function sortByClauseNumber(c1, c2) {
|
---|
807 | var c1c = c1.number.split('.');
|
---|
808 | var c2c = c2.number.split('.');
|
---|
809 |
|
---|
810 | for (var i = 0; i < c1c.length; i++) {
|
---|
811 | if (i >= c2c.length) {
|
---|
812 | return 1;
|
---|
813 | }
|
---|
814 |
|
---|
815 | var c1 = c1c[i];
|
---|
816 | var c2 = c2c[i];
|
---|
817 | var c1cn = Number(c1);
|
---|
818 | var c2cn = Number(c2);
|
---|
819 |
|
---|
820 | if (Number.isNaN(c1cn) && Number.isNaN(c2cn)) {
|
---|
821 | if (c1 > c2) {
|
---|
822 | return 1;
|
---|
823 | } else if (c1 < c2) {
|
---|
824 | return -1;
|
---|
825 | }
|
---|
826 | } else if (!Number.isNaN(c1cn) && Number.isNaN(c2cn)) {
|
---|
827 | return -1;
|
---|
828 | } else if (Number.isNaN(c1cn) && !Number.isNaN(c2cn)) {
|
---|
829 | return 1;
|
---|
830 | } else if(c1cn > c2cn) {
|
---|
831 | return 1;
|
---|
832 | } else if (c1cn < c2cn) {
|
---|
833 | return -1;
|
---|
834 | }
|
---|
835 | }
|
---|
836 |
|
---|
837 | if (c1c.length === c2c.length) {
|
---|
838 | return 0;
|
---|
839 | }
|
---|
840 | return -1;
|
---|
841 | }
|
---|
842 |
|
---|
843 | document.addEventListener('DOMContentLoaded', function () {
|
---|
844 | Toolbox.init();
|
---|
845 | referencePane.init();
|
---|
846 | })
|
---|
847 | var CLAUSE_NODES = ['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'];
|
---|
848 | function findLocalReferences ($elem) {
|
---|
849 | var name = $elem.innerHTML;
|
---|
850 | var references = [];
|
---|
851 |
|
---|
852 | var parentClause = $elem.parentNode;
|
---|
853 | while (parentClause && CLAUSE_NODES.indexOf(parentClause.nodeName) === -1) {
|
---|
854 | parentClause = parentClause.parentNode;
|
---|
855 | }
|
---|
856 |
|
---|
857 | if(!parentClause) return;
|
---|
858 |
|
---|
859 | var vars = parentClause.querySelectorAll('var');
|
---|
860 |
|
---|
861 | for (var i = 0; i < vars.length; i++) {
|
---|
862 | var $var = vars[i];
|
---|
863 |
|
---|
864 | if ($var.innerHTML === name) {
|
---|
865 | references.push($var);
|
---|
866 | }
|
---|
867 | }
|
---|
868 |
|
---|
869 | return references;
|
---|
870 | }
|
---|
871 |
|
---|
872 | function toggleFindLocalReferences($elem) {
|
---|
873 | var references = findLocalReferences($elem);
|
---|
874 | if ($elem.classList.contains('referenced')) {
|
---|
875 | references.forEach(function ($reference) {
|
---|
876 | $reference.classList.remove('referenced');
|
---|
877 | });
|
---|
878 | } else {
|
---|
879 | references.forEach(function ($reference) {
|
---|
880 | $reference.classList.add('referenced');
|
---|
881 | });
|
---|
882 | }
|
---|
883 | }
|
---|
884 |
|
---|
885 | function installFindLocalReferences () {
|
---|
886 | document.addEventListener('click', function (e) {
|
---|
887 | if (e.target.nodeName === 'VAR') {
|
---|
888 | toggleFindLocalReferences(e.target);
|
---|
889 | }
|
---|
890 | });
|
---|
891 | }
|
---|
892 |
|
---|
893 | document.addEventListener('DOMContentLoaded', installFindLocalReferences);
|
---|