UNPKG

ecmarkup

Version:

Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.

1,280 lines (1,119 loc) 37.6 kB
'use strict'; function Search(menu) { this.menu = menu; this.$search = document.getElementById('menu-search'); this.$searchBox = document.getElementById('menu-search-box'); this.$searchResults = document.getElementById('menu-search-results'); this.loadBiblio(); document.addEventListener('keydown', this.documentKeydown.bind(this)); this.$searchBox.addEventListener( 'keydown', debounce(this.searchBoxKeydown.bind(this), { stopPropagation: true }), ); this.$searchBox.addEventListener( 'keyup', debounce(this.searchBoxKeyup.bind(this), { stopPropagation: true }), ); // Perform an initial search if the box is not empty. if (this.$searchBox.value) { this.search(this.$searchBox.value); } } Search.prototype.loadBiblio = function () { if (typeof biblio === 'undefined') { console.error('could not find biblio'); this.biblio = { refToClause: {}, entries: [] }; } else { this.biblio = biblio; this.biblio.clauses = this.biblio.entries.filter(e => e.type === 'clause'); this.biblio.byId = this.biblio.entries.reduce((map, entry) => { map[entry.id] = entry; return map; }, {}); let refParentClause = Object.create(null); this.biblio.refParentClause = refParentClause; let refsByClause = this.biblio.refsByClause; Object.keys(refsByClause).forEach(clause => { refsByClause[clause].forEach(ref => { refParentClause[ref] = clause; }); }); } }; Search.prototype.documentKeydown = function (e) { if (e.key === '/') { e.preventDefault(); e.stopPropagation(); this.triggerSearch(); } }; Search.prototype.searchBoxKeydown = function (e) { e.stopPropagation(); e.preventDefault(); if (e.keyCode === 191 && e.target.value.length === 0) { e.preventDefault(); } else if (e.keyCode === 13) { e.preventDefault(); this.selectResult(); } }; Search.prototype.searchBoxKeyup = function (e) { if (e.keyCode === 13 || e.keyCode === 9) { return; } this.search(e.target.value); }; Search.prototype.triggerSearch = function () { if (this.menu.isVisible()) { this._closeAfterSearch = false; } else { this._closeAfterSearch = true; this.menu.show(); } this.$searchBox.focus(); this.$searchBox.select(); }; // bit 12 - Set if the result starts with searchString // bits 8-11: 8 - number of chunks multiplied by 2 if cases match, otherwise 1. // bits 1-7: 127 - length of the entry // General scheme: prefer case sensitive matches with fewer chunks, and otherwise // prefer shorter matches. function relevance(result) { let relevance = 0; relevance = Math.max(0, 8 - result.match.chunks) << 7; if (result.match.caseMatch) { relevance *= 2; } if (result.match.prefix) { relevance += 2048; } relevance += Math.max(0, 255 - result.key.length); return relevance; } Search.prototype.search = function (searchString) { if (searchString === '') { this.displayResults([]); this.hideSearch(); return; } else { this.showSearch(); } if (searchString.length === 1) { this.displayResults([]); return; } let results; if (/^[\d.]*$/.test(searchString)) { results = this.biblio.clauses .filter(clause => clause.number.substring(0, searchString.length) === searchString) .map(clause => ({ key: getKey(clause), entry: clause })); } else { results = []; for (let i = 0; i < this.biblio.entries.length; i++) { let entry = this.biblio.entries[i]; let key = getKey(entry); if (!key) { // biblio entries without a key aren't searchable continue; } let match = fuzzysearch(searchString, key); if (match) { results.push({ key, entry, match }); } } results.forEach(result => { result.relevance = relevance(result, searchString); }); results = results.sort((a, b) => b.relevance - a.relevance); } if (results.length > 50) { results = results.slice(0, 50); } this.displayResults(results); }; Search.prototype.hideSearch = function () { this.$search.classList.remove('active'); }; Search.prototype.showSearch = function () { this.$search.classList.add('active'); }; Search.prototype.selectResult = function () { let $first = this.$searchResults.querySelector('li:first-child a'); if ($first) { document.location = $first.getAttribute('href'); } this.$searchBox.value = ''; this.$searchBox.blur(); this.displayResults([]); this.hideSearch(); if (this._closeAfterSearch) { this.menu.hide(); } }; Search.prototype.displayResults = function (results) { if (results.length > 0) { this.$searchResults.classList.remove('no-results'); let html = '<ul>'; results.forEach(result => { let key = result.key; let entry = result.entry; let id = entry.id; let cssClass = ''; let text = ''; if (entry.type === 'clause') { let number = entry.number ? entry.number + ' ' : ''; text = number + key; cssClass = 'clause'; id = entry.id; } else if (entry.type === 'production') { text = key; cssClass = 'prod'; id = entry.id; } else if (entry.type === 'op') { text = key; cssClass = 'op'; id = entry.id || entry.refId; } else if (entry.type === 'term') { text = key; cssClass = 'term'; id = entry.id || entry.refId; } if (text) { html += `<li class=menu-search-result-${cssClass}><a href="${makeLinkToId(id)}">${text}</a></li>`; } }); html += '</ul>'; this.$searchResults.innerHTML = html; } else { this.$searchResults.innerHTML = ''; this.$searchResults.classList.add('no-results'); } }; function getKey(item) { if (item.key) { return item.key; } switch (item.type) { case 'clause': return item.title || item.titleHTML; case 'production': return item.name; case 'op': return item.aoid; case 'term': return item.term; case 'table': case 'figure': case 'example': case 'note': return item.caption; case 'step': return item.id; default: throw new Error("Can't get key for " + item.type); } } function Menu() { this.$toggle = document.getElementById('menu-toggle'); this.$menu = document.getElementById('menu'); this.$toc = document.querySelector('menu-toc > ol'); this.$pins = document.querySelector('#menu-pins'); this.$pinList = document.getElementById('menu-pins-list'); this.$toc = document.querySelector('#menu-toc > ol'); this.$specContainer = document.getElementById('spec-container'); this.search = new Search(this); this._pinnedIds = {}; this.loadPinEntries(); // unpin all button document .querySelector('#menu-pins .unpin-all') .addEventListener('click', this.unpinAll.bind(this)); // individual unpinning buttons this.$pinList.addEventListener('click', this.pinListClick.bind(this)); // toggle menu this.$toggle.addEventListener('click', this.toggle.bind(this)); // keydown events for pinned clauses document.addEventListener('keydown', this.documentKeydown.bind(this)); // toc expansion let tocItems = this.$menu.querySelectorAll('#menu-toc li'); for (let i = 0; i < tocItems.length; i++) { let $item = tocItems[i]; $item.addEventListener('click', event => { $item.classList.toggle('active'); event.stopPropagation(); }); } // close toc on toc item selection let tocLinks = this.$menu.querySelectorAll('#menu-toc li > a'); for (let i = 0; i < tocLinks.length; i++) { let $link = tocLinks[i]; $link.addEventListener('click', event => { this.toggle(); event.stopPropagation(); }); } // update active clause on scroll window.addEventListener('scroll', debounce(this.updateActiveClause.bind(this))); this.updateActiveClause(); // prevent menu scrolling from scrolling the body this.$toc.addEventListener('wheel', e => { let target = e.currentTarget; let offTop = e.deltaY < 0 && target.scrollTop === 0; if (offTop) { e.preventDefault(); } let offBottom = e.deltaY > 0 && target.offsetHeight + target.scrollTop >= target.scrollHeight; if (offBottom) { e.preventDefault(); } }); } Menu.prototype.documentKeydown = function (e) { e.stopPropagation(); if (e.key === 'p') { this.togglePinEntry(); } else if ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].includes(parseInt(e.key))) { this.selectPin((e.keyCode - 9) % 10); } else if (e.key === '`') { const hash = document.location.hash; const id = decodeURIComponent(hash.slice(1)); const target = document.getElementById(id); target?.scrollIntoView(true); } }; Menu.prototype.updateActiveClause = function () { this.setActiveClause(findActiveClause(this.$specContainer)); }; Menu.prototype.setActiveClause = function (clause) { this.$activeClause = clause; this.revealInToc(this.$activeClause); }; Menu.prototype.revealInToc = function (path) { let current = this.$toc.querySelectorAll('li.revealed'); for (let i = 0; i < current.length; i++) { current[i].classList.remove('revealed'); current[i].classList.remove('revealed-leaf'); } current = this.$toc; let index = 0; outer: while (index < path.length) { let children = current.children; for (let i = 0; i < children.length; i++) { if ('#' + path[index].id === children[i].children[1].hash) { children[i].classList.add('revealed'); if (index === path.length - 1) { children[i].classList.add('revealed-leaf'); let rect = children[i].getBoundingClientRect(); // this.$toc.getBoundingClientRect().top; let tocRect = this.$toc.getBoundingClientRect(); if (rect.top + 10 > tocRect.bottom) { this.$toc.scrollTop = this.$toc.scrollTop + (rect.top - tocRect.bottom) + (rect.bottom - rect.top); } else if (rect.top < tocRect.top) { this.$toc.scrollTop = this.$toc.scrollTop - (tocRect.top - rect.top); } } current = children[i].querySelector('ol'); index++; continue outer; } } console.log('could not find location in table of contents', path); break; } }; function findActiveClause(root, path) { path = path || []; let visibleClauses = getVisibleClauses(root, path); let midpoint = Math.floor(window.innerHeight / 2); for (let [$clause, path] of visibleClauses) { let { top: clauseTop, bottom: clauseBottom } = $clause.getBoundingClientRect(); let isFullyVisibleAboveTheFold = clauseTop > 0 && clauseTop < midpoint && clauseBottom < window.innerHeight; if (isFullyVisibleAboveTheFold) { return path; } } visibleClauses.sort(([, pathA], [, pathB]) => pathB.length - pathA.length); for (let [$clause, path] of visibleClauses) { let { top: clauseTop, bottom: clauseBottom } = $clause.getBoundingClientRect(); let $header = $clause.querySelector('h1'); let clauseStyles = getComputedStyle($clause); let marginTop = Math.max( 0, parseInt(clauseStyles['margin-top']), parseInt(getComputedStyle($header)['margin-top']), ); let marginBottom = Math.max(0, parseInt(clauseStyles['margin-bottom'])); let crossesMidpoint = clauseTop - marginTop <= midpoint && clauseBottom + marginBottom >= midpoint; if (crossesMidpoint) { return path; } } return path; } function getVisibleClauses(root, path) { let childClauses = getChildClauses(root); path = path || []; let result = []; let seenVisibleClause = false; for (let $clause of childClauses) { let { top: clauseTop, bottom: clauseBottom } = $clause.getBoundingClientRect(); let isPartiallyVisible = (clauseTop > 0 && clauseTop < window.innerHeight) || (clauseBottom > 0 && clauseBottom < window.innerHeight) || (clauseTop < 0 && clauseBottom > window.innerHeight); if (isPartiallyVisible) { seenVisibleClause = true; let innerPath = path.concat($clause); result.push([$clause, innerPath]); result.push(...getVisibleClauses($clause, innerPath)); } else if (seenVisibleClause) { break; } } return result; } function* getChildClauses(root) { for (let el of root.children) { switch (el.nodeName) { // descend into <emu-import> case 'EMU-IMPORT': yield* getChildClauses(el); break; // accept <emu-clause>, <emu-intro>, and <emu-annex> case 'EMU-CLAUSE': case 'EMU-INTRO': case 'EMU-ANNEX': yield el; } } } Menu.prototype.toggle = function () { this.$menu.classList.toggle('active'); }; Menu.prototype.show = function () { this.$menu.classList.add('active'); }; Menu.prototype.hide = function () { this.$menu.classList.remove('active'); }; Menu.prototype.isVisible = function () { return this.$menu.classList.contains('active'); }; Menu.prototype.showPins = function () { this.$pins.classList.add('active'); }; Menu.prototype.hidePins = function () { this.$pins.classList.remove('active'); }; Menu.prototype.addPinEntry = function (id) { let entry = this.search.biblio.byId[id]; if (!entry) { // id was deleted after pin (or something) so remove it delete this._pinnedIds[id]; this.persistPinEntries(); return; } let text; if (entry.type === 'clause') { let prefix; if (entry.number) { prefix = entry.number + ' '; } else { prefix = ''; } text = `${prefix}${entry.titleHTML}`; } else { text = getKey(entry); } let link = `<a href="${makeLinkToId(entry.id)}">${text}</a>`; this.$pinList.innerHTML += `<li data-section-id="${id}">${link}<button class="unpin">\u{2716}</button></li>`; if (Object.keys(this._pinnedIds).length === 0) { this.showPins(); } this._pinnedIds[id] = true; this.persistPinEntries(); }; Menu.prototype.removePinEntry = function (id) { let item = this.$pinList.querySelector(`li[data-section-id="${id}"]`); this.$pinList.removeChild(item); delete this._pinnedIds[id]; if (Object.keys(this._pinnedIds).length === 0) { this.hidePins(); } this.persistPinEntries(); }; Menu.prototype.unpinAll = function () { for (let id of Object.keys(this._pinnedIds)) { this.removePinEntry(id); } }; Menu.prototype.pinListClick = function (event) { if (event?.target?.classList.contains('unpin')) { let id = event.target.parentNode.dataset.sectionId; if (id) { this.removePinEntry(id); } } }; Menu.prototype.persistPinEntries = function () { try { if (!window.localStorage) return; } catch (e) { return; } localStorage.pinEntries = JSON.stringify(Object.keys(this._pinnedIds)); }; Menu.prototype.loadPinEntries = function () { try { if (!window.localStorage) return; } catch (e) { return; } let pinsString = window.localStorage.pinEntries; if (!pinsString) return; let pins = JSON.parse(pinsString); for (let i = 0; i < pins.length; i++) { this.addPinEntry(pins[i]); } }; Menu.prototype.togglePinEntry = function (id) { if (!id) { id = this.$activeClause[this.$activeClause.length - 1].id; } if (this._pinnedIds[id]) { this.removePinEntry(id); } else { this.addPinEntry(id); } }; Menu.prototype.selectPin = function (num) { if (num >= this.$pinList.children.length) return; document.location = this.$pinList.children[num].children[0].href; }; let menu; document.addEventListener('DOMContentLoaded', init); function debounce(fn, opts) { opts = opts || {}; let timeout; return function (e) { if (opts.stopPropagation) { e.stopPropagation(); } let args = arguments; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { timeout = null; fn.apply(this, args); }, 150); }; } let CLAUSE_NODES = ['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX']; function findContainer($elem) { let parentClause = $elem.parentNode; while (parentClause && CLAUSE_NODES.indexOf(parentClause.nodeName) === -1) { parentClause = parentClause.parentNode; } return parentClause; } function findLocalReferences(parentClause, name) { let vars = parentClause.querySelectorAll('var'); let references = []; for (let i = 0; i < vars.length; i++) { let $var = vars[i]; if ($var.innerHTML === name) { references.push($var); } } return references; } let REFERENCED_CLASSES = Array.from({ length: 7 }, (x, i) => `referenced${i}`); function chooseHighlightIndex(parentClause) { let counts = REFERENCED_CLASSES.map($class => parentClause.getElementsByClassName($class).length); // Find the earliest index with the lowest count. let minCount = Infinity; let index = null; for (let i = 0; i < counts.length; i++) { if (counts[i] < minCount) { minCount = counts[i]; index = i; } } return index; } function toggleFindLocalReferences($elem) { let parentClause = findContainer($elem); let references = findLocalReferences(parentClause, $elem.innerHTML); if ($elem.classList.contains('referenced')) { references.forEach($reference => { $reference.classList.remove('referenced', ...REFERENCED_CLASSES); }); } else { let index = chooseHighlightIndex(parentClause); references.forEach($reference => { $reference.classList.add('referenced', `referenced${index}`); }); } } function installFindLocalReferences() { document.addEventListener('click', e => { if (e.target.nodeName === 'VAR') { toggleFindLocalReferences(e.target); } }); } document.addEventListener('DOMContentLoaded', installFindLocalReferences); // The following license applies to the fuzzysearch function // The MIT License (MIT) // Copyright © 2015 Nicolas Bevacqua // Copyright © 2016 Brian Terlson // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. function fuzzysearch(searchString, haystack, caseInsensitive) { let tlen = haystack.length; let qlen = searchString.length; let chunks = 1; let finding = false; if (qlen > tlen) { return false; } if (qlen === tlen) { if (searchString === haystack) { return { caseMatch: true, chunks: 1, prefix: true }; } else if (searchString.toLowerCase() === haystack.toLowerCase()) { return { caseMatch: false, chunks: 1, prefix: true }; } else { return false; } } let j = 0; outer: for (let i = 0; i < qlen; i++) { let nch = searchString[i]; while (j < tlen) { let targetChar = haystack[j++]; if (targetChar === nch) { finding = true; continue outer; } if (finding) { chunks++; finding = false; } } if (caseInsensitive) { return false; } return fuzzysearch(searchString.toLowerCase(), haystack.toLowerCase(), true); } return { caseMatch: !caseInsensitive, chunks, prefix: j <= qlen }; } let referencePane = { init() { this.$container = document.createElement('div'); this.$container.setAttribute('id', 'references-pane-container'); let $spacer = document.createElement('div'); $spacer.setAttribute('id', 'references-pane-spacer'); $spacer.classList.add('menu-spacer'); this.$pane = document.createElement('div'); this.$pane.setAttribute('id', 'references-pane'); this.$container.appendChild($spacer); this.$container.appendChild(this.$pane); this.$header = document.createElement('div'); this.$header.classList.add('menu-pane-header'); this.$headerText = document.createElement('span'); this.$header.appendChild(this.$headerText); this.$headerRefId = document.createElement('a'); this.$header.appendChild(this.$headerRefId); this.$header.addEventListener('pointerdown', e => { this.dragStart(e); }); this.$closeButton = document.createElement('span'); this.$closeButton.setAttribute('id', 'references-pane-close'); this.$closeButton.addEventListener('click', () => { this.deactivate(); }); this.$header.appendChild(this.$closeButton); this.$pane.appendChild(this.$header); this.$tableContainer = document.createElement('div'); this.$tableContainer.setAttribute('id', 'references-pane-table-container'); this.$table = document.createElement('table'); this.$table.setAttribute('id', 'references-pane-table'); this.$tableBody = this.$table.createTBody(); this.$tableContainer.appendChild(this.$table); this.$pane.appendChild(this.$tableContainer); if (menu != null) { menu.$specContainer.appendChild(this.$container); } }, activate() { this.$container.classList.add('active'); }, deactivate() { this.$container.classList.remove('active'); this.state = null; }, showReferencesFor(entry) { this.activate(); this.state = { type: 'ref', id: entry.id }; this.$headerText.textContent = 'References to '; let newBody = document.createElement('tbody'); let previousId; let previousCell; let dupCount = 0; this.$headerRefId.innerHTML = getKey(entry); this.$headerRefId.setAttribute('href', makeLinkToId(entry.id)); this.$headerRefId.style.display = 'inline'; (entry.referencingIds || []) .map(id => { let cid = menu.search.biblio.refParentClause[id]; let clause = menu.search.biblio.byId[cid]; if (clause == null) { throw new Error('could not find clause for id ' + cid); } return { id, clause }; }) .sort((a, b) => sortByClauseNumber(a.clause, b.clause)) .forEach(record => { if (previousId === record.clause.id) { previousCell.innerHTML += ` (<a href="${makeLinkToId(record.id)}">${dupCount + 2}</a>)`; dupCount++; } else { let row = newBody.insertRow(); let cell = row.insertCell(); cell.innerHTML = record.clause.number; cell = row.insertCell(); cell.innerHTML = `<a href="${makeLinkToId(record.id)}">${record.clause.titleHTML}</a>`; previousCell = cell; previousId = record.clause.id; dupCount = 0; } }, this); this.$table.removeChild(this.$tableBody); this.$tableBody = newBody; this.$table.appendChild(this.$tableBody); this.autoSize(); }, showSDOs(sdos, alternativeId) { let rhs = document.getElementById(alternativeId); let parentName = rhs.parentNode.getAttribute('name'); let colons = rhs.parentNode.querySelector('emu-geq'); rhs = rhs.cloneNode(true); rhs.querySelectorAll('emu-params,emu-constraints').forEach(e => { e.remove(); }); rhs.querySelectorAll('[id]').forEach(e => { e.removeAttribute('id'); }); rhs.querySelectorAll('a').forEach(e => { e.parentNode.replaceChild(document.createTextNode(e.textContent), e); }); 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>`; this.$headerText.querySelector('a').append(rhs); this.showSDOsBody(sdos, alternativeId); }, showSDOsBody(sdos, alternativeId) { this.activate(); this.state = { type: 'sdo', id: alternativeId, html: this.$headerText.innerHTML }; this.$headerRefId.style.display = 'none'; let newBody = document.createElement('tbody'); Object.keys(sdos).forEach(sdoName => { let pair = sdos[sdoName]; let clause = pair.clause; let ids = pair.ids; let first = ids[0]; let row = newBody.insertRow(); let cell = row.insertCell(); cell.innerHTML = clause; cell = row.insertCell(); let html = '<a href="' + makeLinkToId(first) + '">' + sdoName + '</a>'; for (let i = 1; i < ids.length; ++i) { html += ' (<a href="' + makeLinkToId(ids[i]) + '">' + (i + 1) + '</a>)'; } cell.innerHTML = html; }); this.$table.removeChild(this.$tableBody); this.$tableBody = newBody; this.$table.appendChild(this.$tableBody); this.autoSize(); }, autoSize() { this.$tableContainer.style.height = Math.min(250, this.$table.getBoundingClientRect().height) + 'px'; }, dragStart(pointerDownEvent) { let startingMousePos = pointerDownEvent.clientY; let startingHeight = this.$tableContainer.getBoundingClientRect().height; let moveListener = pointerMoveEvent => { if (pointerMoveEvent.buttons === 0) { removeListeners(); return; } let desiredHeight = startingHeight - (pointerMoveEvent.clientY - startingMousePos); this.$tableContainer.style.height = Math.max(0, desiredHeight) + 'px'; }; let listenerOptions = { capture: true, passive: true }; let removeListeners = () => { document.removeEventListener('pointermove', moveListener, listenerOptions); this.$header.removeEventListener('pointerup', removeListeners, listenerOptions); this.$header.removeEventListener('pointercancel', removeListeners, listenerOptions); }; document.addEventListener('pointermove', moveListener, listenerOptions); this.$header.addEventListener('pointerup', removeListeners, listenerOptions); this.$header.addEventListener('pointercancel', removeListeners, listenerOptions); }, }; let Toolbox = { init() { this.$outer = document.createElement('div'); this.$outer.classList.add('toolbox-container'); this.$container = document.createElement('div'); this.$container.classList.add('toolbox'); this.$outer.appendChild(this.$container); this.$permalink = document.createElement('a'); this.$permalink.textContent = 'Permalink'; this.$pinLink = document.createElement('a'); this.$pinLink.textContent = 'Pin'; this.$pinLink.setAttribute('href', '#'); this.$pinLink.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); menu.togglePinEntry(this.entry.id); this.$pinLink.textContent = menu._pinnedIds[this.entry.id] ? 'Unpin' : 'Pin'; }); this.$refsLink = document.createElement('a'); this.$refsLink.setAttribute('href', '#'); this.$refsLink.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); referencePane.showReferencesFor(this.entry); }); this.$container.appendChild(this.$permalink); this.$container.appendChild(document.createTextNode(' ')); this.$container.appendChild(this.$pinLink); this.$container.appendChild(document.createTextNode(' ')); this.$container.appendChild(this.$refsLink); document.body.appendChild(this.$outer); }, activate(el, entry, target) { if (el === this._activeEl) return; sdoBox.deactivate(); this.active = true; this.entry = entry; this.$pinLink.textContent = menu._pinnedIds[entry.id] ? 'Unpin' : 'Pin'; this.$outer.classList.add('active'); this.top = el.offsetTop - this.$outer.offsetHeight; this.left = el.offsetLeft - 10; this.$outer.setAttribute('style', 'left: ' + this.left + 'px; top: ' + this.top + 'px'); this.updatePermalink(); this.updateReferences(); this._activeEl = el; if (this.top < document.body.scrollTop && el === target) { // don't scroll unless it's a small thing (< 200px) this.$outer.scrollIntoView(); } }, updatePermalink() { this.$permalink.setAttribute('href', makeLinkToId(this.entry.id)); }, updateReferences() { this.$refsLink.textContent = `References (${(this.entry.referencingIds || []).length})`; }, activateIfMouseOver(e) { let ref = this.findReferenceUnder(e.target); if (ref && (!this.active || e.pageY > this._activeEl.offsetTop)) { let entry = menu.search.biblio.byId[ref.id]; this.activate(ref.element, entry, e.target); } else if ( this.active && (e.pageY < this.top || e.pageY > this._activeEl.offsetTop + this._activeEl.offsetHeight) ) { this.deactivate(); } }, findReferenceUnder(el) { while (el) { let parent = el.parentNode; if (el.nodeName === 'EMU-RHS' || el.nodeName === 'EMU-PRODUCTION') { return null; } if ( el.nodeName === 'H1' && parent.nodeName.match(/EMU-CLAUSE|EMU-ANNEX|EMU-INTRO/) && parent.id ) { return { element: el, id: parent.id }; } else if (el.nodeName === 'EMU-NT') { if ( parent.nodeName === 'EMU-PRODUCTION' && parent.id && parent.id[0] !== '_' && parent.firstElementChild === el ) { // return the LHS non-terminal element return { element: el, id: parent.id }; } return null; } else if ( el.nodeName.match(/EMU-(?!CLAUSE|XREF|ANNEX|INTRO)|DFN/) && el.id && el.id[0] !== '_' ) { if ( el.nodeName === 'EMU-FIGURE' || el.nodeName === 'EMU-TABLE' || el.nodeName === 'EMU-EXAMPLE' ) { // return the figcaption element return { element: el.children[0].children[0], id: el.id }; } else { return { element: el, id: el.id }; } } el = parent; } }, deactivate() { this.$outer.classList.remove('active'); this._activeEl = null; this.active = false; }, }; function sortByClauseNumber(clause1, clause2) { let c1c = clause1.number.split('.'); let c2c = clause2.number.split('.'); for (let i = 0; i < c1c.length; i++) { if (i >= c2c.length) { return 1; } let c1 = c1c[i]; let c2 = c2c[i]; let c1cn = Number(c1); let c2cn = Number(c2); if (Number.isNaN(c1cn) && Number.isNaN(c2cn)) { if (c1 > c2) { return 1; } else if (c1 < c2) { return -1; } } else if (!Number.isNaN(c1cn) && Number.isNaN(c2cn)) { return -1; } else if (Number.isNaN(c1cn) && !Number.isNaN(c2cn)) { return 1; } else if (c1cn > c2cn) { return 1; } else if (c1cn < c2cn) { return -1; } } if (c1c.length === c2c.length) { return 0; } return -1; } function makeLinkToId(id) { let hash = '#' + id; if (typeof idToSection === 'undefined' || !idToSection[id]) { return hash; } let targetSec = idToSection[id]; return (targetSec === 'index' ? './' : targetSec + '.html') + hash; } function doShortcut(e) { if (!(e.target instanceof HTMLElement)) { return; } let target = e.target; let name = target.nodeName.toLowerCase(); if (name === 'textarea' || name === 'input' || name === 'select' || target.isContentEditable) { return; } if (e.altKey || e.ctrlKey || e.metaKey) { return; } if (e.key === 'm' && usesMultipage) { let pathParts = location.pathname.split('/'); let hash = location.hash; if (pathParts[pathParts.length - 2] === 'multipage') { if (hash === '') { let sectionName = pathParts[pathParts.length - 1]; if (sectionName.endsWith('.html')) { sectionName = sectionName.slice(0, -5); } if (idToSection['sec-' + sectionName] !== undefined) { hash = '#sec-' + sectionName; } } location = pathParts.slice(0, -2).join('/') + '/' + hash; } else { location = 'multipage/' + hash; } } else if (e.key === 'u') { document.documentElement.classList.toggle('show-ao-annotations'); } else if (e.key === '?') { document.getElementById('shortcuts-help').classList.toggle('active'); } } function init() { if (document.getElementById('menu') == null) { return; } menu = new Menu(); let $container = document.getElementById('spec-container'); $container.addEventListener( 'mouseover', debounce(e => { Toolbox.activateIfMouseOver(e); }), ); document.addEventListener( 'keydown', debounce(e => { if (e.code === 'Escape') { if (Toolbox.active) { Toolbox.deactivate(); } document.getElementById('shortcuts-help').classList.remove('active'); } }), ); } document.addEventListener('keypress', doShortcut); document.addEventListener('DOMContentLoaded', () => { Toolbox.init(); referencePane.init(); }); // preserve state during navigation function getTocPath(li) { let path = []; let pointer = li; while (true) { let parent = pointer.parentElement; if (parent == null) { return null; } let index = [].indexOf.call(parent.children, pointer); if (index == -1) { return null; } path.unshift(index); pointer = parent.parentElement; if (pointer == null) { return null; } if (pointer.id === 'menu-toc') { break; } if (pointer.tagName !== 'LI') { return null; } } return path; } function activateTocPath(path) { try { let pointer = document.getElementById('menu-toc'); for (let index of path) { pointer = pointer.querySelector('ol').children[index]; } pointer.classList.add('active'); } catch (e) { // pass } } function getActiveTocPaths() { return [...menu.$menu.querySelectorAll('.active')].map(getTocPath).filter(p => p != null); } function initTOCExpansion(visibleItemLimit) { // Initialize to a reasonable amount of TOC expansion: // * Expand any full-breadth nesting level up to visibleItemLimit. // * Expand any *single-item* level while under visibleItemLimit (even if that pushes over it). // Limit to initialization by bailing out if any parent item is already expanded. const tocItems = Array.from(document.querySelectorAll('#menu-toc li')); if (tocItems.some(li => li.classList.contains('active') && li.querySelector('li'))) { return; } const selfAndSiblings = maybe => Array.from(maybe?.parentNode.children ?? []); let currentLevelItems = selfAndSiblings(tocItems[0]); let availableCount = visibleItemLimit - currentLevelItems.length; while (availableCount > 0 && currentLevelItems.length) { const nextLevelItems = currentLevelItems.flatMap(li => selfAndSiblings(li.querySelector('li'))); availableCount -= nextLevelItems.length; if (availableCount > 0 || currentLevelItems.length === 1) { // Expand parent items of the next level down (i.e., current-level items with children). for (const ol of new Set(nextLevelItems.map(li => li.parentNode))) { ol.closest('li').classList.add('active'); } } currentLevelItems = nextLevelItems; } } function initState() { if (typeof menu === 'undefined' || window.navigating) { return; } const storage = typeof sessionStorage !== 'undefined' ? sessionStorage : Object.create(null); if (storage.referencePaneState != null) { let state = JSON.parse(storage.referencePaneState); if (state != null) { if (state.type === 'ref') { let entry = menu.search.biblio.byId[state.id]; if (entry != null) { referencePane.showReferencesFor(entry); } } else if (state.type === 'sdo') { let sdos = sdoMap[state.id]; if (sdos != null) { referencePane.$headerText.innerHTML = state.html; referencePane.showSDOsBody(sdos, state.id); } } delete storage.referencePaneState; } } if (storage.activeTocPaths != null) { document.querySelectorAll('#menu-toc li.active').forEach(li => li.classList.remove('active')); let active = JSON.parse(storage.activeTocPaths); active.forEach(activateTocPath); delete storage.activeTocPaths; } else { initTOCExpansion(20); } if (storage.searchValue != null) { let value = JSON.parse(storage.searchValue); menu.search.$searchBox.value = value; menu.search.search(value); delete storage.searchValue; } if (storage.tocScroll != null) { let tocScroll = JSON.parse(storage.tocScroll); menu.$toc.scrollTop = tocScroll; delete storage.tocScroll; } } document.addEventListener('DOMContentLoaded', initState); window.addEventListener('pageshow', initState); window.addEventListener('beforeunload', () => { if (!window.sessionStorage || typeof menu === 'undefined') { return; } sessionStorage.referencePaneState = JSON.stringify(referencePane.state || null); sessionStorage.activeTocPaths = JSON.stringify(getActiveTocPaths()); sessionStorage.searchValue = JSON.stringify(menu.search.$searchBox.value); sessionStorage.tocScroll = JSON.stringify(menu.$toc.scrollTop); });