1 | 'use strict';
|
2 | function 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 |
|
22 | if (this.$searchBox.value) {
|
23 | this.search(this.$searchBox.value);
|
24 | }
|
25 | }
|
26 |
|
27 | Search.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 |
|
49 | Search.prototype.documentKeydown = function (e) {
|
50 | if (e.keyCode === 191) {
|
51 | e.preventDefault();
|
52 | e.stopPropagation();
|
53 | this.triggerSearch();
|
54 | }
|
55 | };
|
56 |
|
57 | Search.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 |
|
68 | Search.prototype.searchBoxKeyup = function (e) {
|
69 | if (e.keyCode === 13 || e.keyCode === 9) {
|
70 | return;
|
71 | }
|
72 |
|
73 | this.search(e.target.value);
|
74 | };
|
75 |
|
76 | Search.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 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | function 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 |
|
110 | Search.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 |
|
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 | };
|
160 | Search.prototype.hideSearch = function () {
|
161 | this.$search.classList.remove('active');
|
162 | };
|
163 |
|
164 | Search.prototype.showSearch = function () {
|
165 | this.$search.classList.add('active');
|
166 | };
|
167 |
|
168 | Search.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 |
|
185 | Search.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 |
|
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 |
|
232 | function 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 |
|
257 | function 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 |
|
271 | this.$toggle.addEventListener('click', this.toggle.bind(this));
|
272 |
|
273 |
|
274 | document.addEventListener('keydown', this.documentKeydown.bind(this));
|
275 |
|
276 |
|
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 |
|
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 |
|
297 | window.addEventListener('scroll', debounce(this.updateActiveClause.bind(this)));
|
298 | this.updateActiveClause();
|
299 |
|
300 |
|
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 |
|
315 | Menu.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 |
|
324 | Menu.prototype.updateActiveClause = function () {
|
325 | this.setActiveClause(findActiveClause(this.$specContainer));
|
326 | };
|
327 |
|
328 | Menu.prototype.setActiveClause = function (clause) {
|
329 | this.$activeClause = clause;
|
330 | this.revealInToc(this.$activeClause);
|
331 | };
|
332 |
|
333 | Menu.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 |
|
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 |
|
369 | function 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 |
|
389 | function* getChildClauses(root) {
|
390 | for (let el of root.children) {
|
391 | switch (el.nodeName) {
|
392 |
|
393 | case 'EMU-IMPORT':
|
394 | yield* getChildClauses(el);
|
395 | break;
|
396 |
|
397 |
|
398 | case 'EMU-CLAUSE':
|
399 | case 'EMU-INTRO':
|
400 | case 'EMU-ANNEX':
|
401 | yield el;
|
402 | }
|
403 | }
|
404 | }
|
405 |
|
406 | Menu.prototype.toggle = function () {
|
407 | this.$menu.classList.toggle('active');
|
408 | };
|
409 |
|
410 | Menu.prototype.show = function () {
|
411 | this.$menu.classList.add('active');
|
412 | };
|
413 |
|
414 | Menu.prototype.hide = function () {
|
415 | this.$menu.classList.remove('active');
|
416 | };
|
417 |
|
418 | Menu.prototype.isVisible = function () {
|
419 | return this.$menu.classList.contains('active');
|
420 | };
|
421 |
|
422 | Menu.prototype.showPins = function () {
|
423 | this.$pins.classList.add('active');
|
424 | };
|
425 |
|
426 | Menu.prototype.hidePins = function () {
|
427 | this.$pins.classList.remove('active');
|
428 | };
|
429 |
|
430 | Menu.prototype.addPinEntry = function (id) {
|
431 | let entry = this.search.biblio.byId[id];
|
432 | if (!entry) {
|
433 |
|
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 |
|
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 |
|
459 | Menu.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 |
|
470 | Menu.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 |
|
480 | Menu.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 |
|
495 | Menu.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 |
|
507 | Menu.prototype.selectPin = function (num) {
|
508 | document.location = this.$pinList.children[num].children[0].href;
|
509 | };
|
510 |
|
511 | let menu;
|
512 |
|
513 | document.addEventListener('DOMContentLoaded', init);
|
514 |
|
515 | function 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 |
|
533 | let CLAUSE_NODES = ['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'];
|
534 | function 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 |
|
542 | function 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 |
|
557 | let REFERENCED_CLASSES = Array.from({ length: 7 }, (x, i) => `referenced${i}`);
|
558 | function chooseHighlightIndex(parentClause) {
|
559 | let counts = REFERENCED_CLASSES.map($class => parentClause.getElementsByClassName($class).length);
|
560 |
|
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 |
|
572 | function 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 |
|
587 | function installFindLocalReferences() {
|
588 | document.addEventListener('click', e => {
|
589 | if (e.target.nodeName === 'VAR') {
|
590 | toggleFindLocalReferences(e.target);
|
591 | }
|
592 | });
|
593 | }
|
594 |
|
595 | document.addEventListener('DOMContentLoaded', installFindLocalReferences);
|
596 |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
617 | function 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 |
|
662 | let 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 |
|
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 |
|
801 | let 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 |
|
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 |
|
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 |
|
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 |
|
921 | function 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 |
|
958 | function 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 |
|
967 | function 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 |
|
1001 | function 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 |
|
1020 | document.addEventListener('keypress', doShortcut);
|
1021 |
|
1022 | document.addEventListener('DOMContentLoaded', () => {
|
1023 | Toolbox.init();
|
1024 | referencePane.init();
|
1025 | });
|