UNPKG

32.5 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.key === '/') {
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 => ({ key: getKey(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 $spacer.classList.add('menu-spacer');
670
671 this.$pane = document.createElement('div');
672 this.$pane.setAttribute('id', 'references-pane');
673
674 this.$container.appendChild($spacer);
675 this.$container.appendChild(this.$pane);
676
677 this.$header = document.createElement('div');
678 this.$header.classList.add('menu-pane-header');
679 this.$headerText = document.createElement('span');
680 this.$header.appendChild(this.$headerText);
681 this.$headerRefId = document.createElement('a');
682 this.$header.appendChild(this.$headerRefId);
683 this.$closeButton = document.createElement('span');
684 this.$closeButton.setAttribute('id', 'references-pane-close');
685 this.$closeButton.addEventListener('click', () => {
686 this.deactivate();
687 });
688 this.$header.appendChild(this.$closeButton);
689
690 this.$pane.appendChild(this.$header);
691 let tableContainer = document.createElement('div');
692 tableContainer.setAttribute('id', 'references-pane-table-container');
693
694 this.$table = document.createElement('table');
695 this.$table.setAttribute('id', 'references-pane-table');
696
697 this.$tableBody = this.$table.createTBody();
698
699 tableContainer.appendChild(this.$table);
700 this.$pane.appendChild(tableContainer);
701
702 menu.$specContainer.appendChild(this.$container);
703 },
704
705 activate() {
706 this.$container.classList.add('active');
707 },
708
709 deactivate() {
710 this.$container.classList.remove('active');
711 this.state = null;
712 },
713
714 showReferencesFor(entry) {
715 this.activate();
716 this.state = { type: 'ref', id: entry.id };
717 this.$headerText.textContent = 'References to ';
718 let newBody = document.createElement('tbody');
719 let previousId;
720 let previousCell;
721 let dupCount = 0;
722 this.$headerRefId.textContent = '#' + entry.id;
723 this.$headerRefId.setAttribute('href', makeLinkToId(entry.id));
724 this.$headerRefId.style.display = 'inline';
725 (entry.referencingIds || [])
726 .map(id => {
727 let cid = menu.search.biblio.refParentClause[id];
728 let clause = menu.search.biblio.byId[cid];
729 if (clause == null) {
730 throw new Error('could not find clause for id ' + cid);
731 }
732 return { id, clause };
733 })
734 .sort((a, b) => sortByClauseNumber(a.clause, b.clause))
735 .forEach(record => {
736 if (previousId === record.clause.id) {
737 previousCell.innerHTML += ` (<a href="${makeLinkToId(record.id)}">${dupCount + 2}</a>)`;
738 dupCount++;
739 } else {
740 let row = newBody.insertRow();
741 let cell = row.insertCell();
742 cell.innerHTML = record.clause.number;
743 cell = row.insertCell();
744 cell.innerHTML = `<a href="${makeLinkToId(record.id)}">${record.clause.titleHTML}</a>`;
745 previousCell = cell;
746 previousId = record.clause.id;
747 dupCount = 0;
748 }
749 }, this);
750 this.$table.removeChild(this.$tableBody);
751 this.$tableBody = newBody;
752 this.$table.appendChild(this.$tableBody);
753 },
754
755 showSDOs(sdos, alternativeId) {
756 let rhs = document.getElementById(alternativeId);
757 let parentName = rhs.parentNode.getAttribute('name');
758 let colons = rhs.parentNode.querySelector('emu-geq');
759 rhs = rhs.cloneNode(true);
760 rhs.querySelectorAll('emu-params,emu-constraints').forEach(e => {
761 e.remove();
762 });
763 rhs.querySelectorAll('[id]').forEach(e => {
764 e.removeAttribute('id');
765 });
766 rhs.querySelectorAll('a').forEach(e => {
767 e.parentNode.replaceChild(document.createTextNode(e.textContent), e);
768 });
769
770 // prettier-ignore
771 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>`;
772 this.$headerText.querySelector('a').append(rhs);
773 this.showSDOsBody(sdos, alternativeId);
774 },
775
776 showSDOsBody(sdos, alternativeId) {
777 this.activate();
778 this.state = { type: 'sdo', id: alternativeId, html: this.$headerText.innerHTML };
779 this.$headerRefId.style.display = 'none';
780 let newBody = document.createElement('tbody');
781 Object.keys(sdos).forEach(sdoName => {
782 let pair = sdos[sdoName];
783 let clause = pair.clause;
784 let ids = pair.ids;
785 let first = ids[0];
786 let row = newBody.insertRow();
787 let cell = row.insertCell();
788 cell.innerHTML = clause;
789 cell = row.insertCell();
790 let html = '<a href="' + makeLinkToId(first) + '">' + sdoName + '</a>';
791 for (let i = 1; i < ids.length; ++i) {
792 html += ' (<a href="' + makeLinkToId(ids[i]) + '">' + (i + 1) + '</a>)';
793 }
794 cell.innerHTML = html;
795 });
796 this.$table.removeChild(this.$tableBody);
797 this.$tableBody = newBody;
798 this.$table.appendChild(this.$tableBody);
799 },
800};
801
802let Toolbox = {
803 init() {
804 this.$outer = document.createElement('div');
805 this.$outer.classList.add('toolbox-container');
806 this.$container = document.createElement('div');
807 this.$container.classList.add('toolbox');
808 this.$outer.appendChild(this.$container);
809 this.$permalink = document.createElement('a');
810 this.$permalink.textContent = 'Permalink';
811 this.$pinLink = document.createElement('a');
812 this.$pinLink.textContent = 'Pin';
813 this.$pinLink.setAttribute('href', '#');
814 this.$pinLink.addEventListener('click', e => {
815 e.preventDefault();
816 e.stopPropagation();
817 menu.togglePinEntry(this.entry.id);
818 this.$pinLink.textContent = menu._pinnedIds[this.entry.id] ? 'Unpin' : 'Pin';
819 });
820
821 this.$refsLink = document.createElement('a');
822 this.$refsLink.setAttribute('href', '#');
823 this.$refsLink.addEventListener('click', e => {
824 e.preventDefault();
825 e.stopPropagation();
826 referencePane.showReferencesFor(this.entry);
827 });
828 this.$container.appendChild(this.$permalink);
829 this.$container.appendChild(this.$pinLink);
830 this.$container.appendChild(this.$refsLink);
831 document.body.appendChild(this.$outer);
832 },
833
834 activate(el, entry, target) {
835 if (el === this._activeEl) return;
836 sdoBox.deactivate();
837 this.active = true;
838 this.entry = entry;
839 this.$pinLink.textContent = menu._pinnedIds[entry.id] ? 'Unpin' : 'Pin';
840 this.$outer.classList.add('active');
841 this.top = el.offsetTop - this.$outer.offsetHeight;
842 this.left = el.offsetLeft - 10;
843 this.$outer.setAttribute('style', 'left: ' + this.left + 'px; top: ' + this.top + 'px');
844 this.updatePermalink();
845 this.updateReferences();
846 this._activeEl = el;
847 if (this.top < document.body.scrollTop && el === target) {
848 // don't scroll unless it's a small thing (< 200px)
849 this.$outer.scrollIntoView();
850 }
851 },
852
853 updatePermalink() {
854 this.$permalink.setAttribute('href', makeLinkToId(this.entry.id));
855 },
856
857 updateReferences() {
858 this.$refsLink.textContent = `References (${(this.entry.referencingIds || []).length})`;
859 },
860
861 activateIfMouseOver(e) {
862 let ref = this.findReferenceUnder(e.target);
863 if (ref && (!this.active || e.pageY > this._activeEl.offsetTop)) {
864 let entry = menu.search.biblio.byId[ref.id];
865 this.activate(ref.element, entry, e.target);
866 } else if (
867 this.active &&
868 (e.pageY < this.top || e.pageY > this._activeEl.offsetTop + this._activeEl.offsetHeight)
869 ) {
870 this.deactivate();
871 }
872 },
873
874 findReferenceUnder(el) {
875 while (el) {
876 let parent = el.parentNode;
877 if (el.nodeName === 'EMU-RHS' || el.nodeName === 'EMU-PRODUCTION') {
878 return null;
879 }
880 if (
881 el.nodeName === 'H1' &&
882 parent.nodeName.match(/EMU-CLAUSE|EMU-ANNEX|EMU-INTRO/) &&
883 parent.id
884 ) {
885 return { element: el, id: parent.id };
886 } else if (el.nodeName === 'EMU-NT') {
887 if (
888 parent.nodeName === 'EMU-PRODUCTION' &&
889 parent.id &&
890 parent.id[0] !== '_' &&
891 parent.firstElementChild === el
892 ) {
893 // return the LHS non-terminal element
894 return { element: el, id: parent.id };
895 }
896 return null;
897 } else if (
898 el.nodeName.match(/EMU-(?!CLAUSE|XREF|ANNEX|INTRO)|DFN/) &&
899 el.id &&
900 el.id[0] !== '_'
901 ) {
902 if (
903 el.nodeName === 'EMU-FIGURE' ||
904 el.nodeName === 'EMU-TABLE' ||
905 el.nodeName === 'EMU-EXAMPLE'
906 ) {
907 // return the figcaption element
908 return { element: el.children[0].children[0], id: el.id };
909 } else {
910 return { element: el, id: el.id };
911 }
912 }
913 el = parent;
914 }
915 },
916
917 deactivate() {
918 this.$outer.classList.remove('active');
919 this._activeEl = null;
920 this.active = false;
921 },
922};
923
924function sortByClauseNumber(clause1, clause2) {
925 let c1c = clause1.number.split('.');
926 let c2c = clause2.number.split('.');
927
928 for (let i = 0; i < c1c.length; i++) {
929 if (i >= c2c.length) {
930 return 1;
931 }
932
933 let c1 = c1c[i];
934 let c2 = c2c[i];
935 let c1cn = Number(c1);
936 let c2cn = Number(c2);
937
938 if (Number.isNaN(c1cn) && Number.isNaN(c2cn)) {
939 if (c1 > c2) {
940 return 1;
941 } else if (c1 < c2) {
942 return -1;
943 }
944 } else if (!Number.isNaN(c1cn) && Number.isNaN(c2cn)) {
945 return -1;
946 } else if (Number.isNaN(c1cn) && !Number.isNaN(c2cn)) {
947 return 1;
948 } else if (c1cn > c2cn) {
949 return 1;
950 } else if (c1cn < c2cn) {
951 return -1;
952 }
953 }
954
955 if (c1c.length === c2c.length) {
956 return 0;
957 }
958 return -1;
959}
960
961function makeLinkToId(id) {
962 let hash = '#' + id;
963 if (typeof idToSection === 'undefined' || !idToSection[id]) {
964 return hash;
965 }
966 let targetSec = idToSection[id];
967 return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
968}
969
970function doShortcut(e) {
971 if (!(e.target instanceof HTMLElement)) {
972 return;
973 }
974 let target = e.target;
975 let name = target.nodeName.toLowerCase();
976 if (name === 'textarea' || name === 'input' || name === 'select' || target.isContentEditable) {
977 return;
978 }
979 if (e.altKey || e.ctrlKey || e.metaKey) {
980 return;
981 }
982 if (e.key === 'm' && usesMultipage) {
983 let pathParts = location.pathname.split('/');
984 let hash = location.hash;
985 if (pathParts[pathParts.length - 2] === 'multipage') {
986 if (hash === '') {
987 let sectionName = pathParts[pathParts.length - 1];
988 if (sectionName.endsWith('.html')) {
989 sectionName = sectionName.slice(0, -5);
990 }
991 if (idToSection['sec-' + sectionName] !== undefined) {
992 hash = '#sec-' + sectionName;
993 }
994 }
995 location = pathParts.slice(0, -2).join('/') + '/' + hash;
996 } else {
997 location = 'multipage/' + hash;
998 }
999 } else if (e.key === 'u') {
1000 document.documentElement.classList.toggle('show-ao-annotations');
1001 } else if (e.key === '?') {
1002 document.getElementById('shortcuts-help').classList.toggle('active');
1003 }
1004}
1005
1006function init() {
1007 menu = new Menu();
1008 let $container = document.getElementById('spec-container');
1009 $container.addEventListener(
1010 'mouseover',
1011 debounce(e => {
1012 Toolbox.activateIfMouseOver(e);
1013 })
1014 );
1015 document.addEventListener(
1016 'keydown',
1017 debounce(e => {
1018 if (e.code === 'Escape') {
1019 if (Toolbox.active) {
1020 Toolbox.deactivate();
1021 }
1022 document.getElementById('shortcuts-help').classList.remove('active');
1023 }
1024 })
1025 );
1026}
1027
1028document.addEventListener('keypress', doShortcut);
1029
1030document.addEventListener('DOMContentLoaded', () => {
1031 Toolbox.init();
1032 referencePane.init();
1033});
1034
1035// preserve state during navigation
1036
1037function getTocPath(li) {
1038 let path = [];
1039 let pointer = li;
1040 while (true) {
1041 let parent = pointer.parentElement;
1042 if (parent == null) {
1043 return null;
1044 }
1045 let index = [].indexOf.call(parent.children, pointer);
1046 if (index == -1) {
1047 return null;
1048 }
1049 path.unshift(index);
1050 pointer = parent.parentElement;
1051 if (pointer == null) {
1052 return null;
1053 }
1054 if (pointer.id === 'menu-toc') {
1055 break;
1056 }
1057 if (pointer.tagName !== 'LI') {
1058 return null;
1059 }
1060 }
1061 return path;
1062}
1063
1064function activateTocPath(path) {
1065 try {
1066 let pointer = document.getElementById('menu-toc');
1067 for (let index of path) {
1068 pointer = pointer.querySelector('ol').children[index];
1069 }
1070 pointer.classList.add('active');
1071 } catch (e) {
1072 // pass
1073 }
1074}
1075
1076function getActiveTocPaths() {
1077 return [...menu.$menu.querySelectorAll('.active')].map(getTocPath).filter(p => p != null);
1078}
1079
1080function loadStateFromSessionStorage() {
1081 if (!window.sessionStorage || typeof menu === 'undefined' || window.navigating) {
1082 return;
1083 }
1084 if (sessionStorage.referencePaneState != null) {
1085 let state = JSON.parse(sessionStorage.referencePaneState);
1086 if (state != null) {
1087 if (state.type === 'ref') {
1088 let entry = menu.search.biblio.byId[state.id];
1089 if (entry != null) {
1090 referencePane.showReferencesFor(entry);
1091 }
1092 } else if (state.type === 'sdo') {
1093 let sdos = sdoMap[state.id];
1094 if (sdos != null) {
1095 referencePane.$headerText.innerHTML = state.html;
1096 referencePane.showSDOsBody(sdos, state.id);
1097 }
1098 }
1099 delete sessionStorage.referencePaneState;
1100 }
1101 }
1102
1103 if (sessionStorage.activeTocPaths != null) {
1104 document
1105 .getElementById('menu-toc')
1106 .querySelectorAll('.active')
1107 .forEach(e => {
1108 e.classList.remove('active');
1109 });
1110 let active = JSON.parse(sessionStorage.activeTocPaths);
1111 active.forEach(activateTocPath);
1112 delete sessionStorage.activeTocPaths;
1113 }
1114
1115 if (sessionStorage.searchValue != null) {
1116 let value = JSON.parse(sessionStorage.searchValue);
1117 menu.search.$searchBox.value = value;
1118 menu.search.search(value);
1119 delete sessionStorage.searchValue;
1120 }
1121
1122 if (sessionStorage.tocScroll != null) {
1123 let tocScroll = JSON.parse(sessionStorage.tocScroll);
1124 menu.$toc.scrollTop = tocScroll;
1125 delete sessionStorage.tocScroll;
1126 }
1127}
1128
1129document.addEventListener('DOMContentLoaded', loadStateFromSessionStorage);
1130
1131window.addEventListener('pageshow', loadStateFromSessionStorage);
1132
1133window.addEventListener('beforeunload', () => {
1134 if (!window.sessionStorage || typeof menu === 'undefined') {
1135 return;
1136 }
1137 sessionStorage.referencePaneState = JSON.stringify(referencePane.state || null);
1138 sessionStorage.activeTocPaths = JSON.stringify(getActiveTocPaths());
1139 sessionStorage.searchValue = JSON.stringify(menu.search.$searchBox.value);
1140 sessionStorage.tocScroll = JSON.stringify(menu.$toc.scrollTop);
1141});