UNPKG

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