diff --git a/.eslintrc.json b/.eslintrc.json index 0e0899c3..83b5cae5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -59,8 +59,9 @@ }, "globals": { "multipageMap": "readonly", - "usesMultipage": "readonly", + "isMultipage": "readonly", "idToSection": "readonly", + "toggleMultipage": "readonly", "sdoMap": "readonly", "biblio": "readonly", "debounce": "writable", diff --git a/css/elements.css b/css/elements.css index 8e0c2a52..b213e7b0 100644 --- a/css/elements.css +++ b/css/elements.css @@ -1675,3 +1675,10 @@ li.menu-search-result-term::before { background: var(--figure-background); width: 500px; } + +/* Other dynamic elements */ +:root[data-multipage-preference=''] [data-multipage-preference=''], +:root[data-multipage-preference='single-page'] [data-multipage-preference='single-page'], +:root[data-multipage-preference='multi-page'] [data-multipage-preference='multi-page'] { + font-weight: bold; +} diff --git a/css/print.css b/css/print.css index 993ce73c..a4f6180b 100644 --- a/css/print.css +++ b/css/print.css @@ -1,3 +1,7 @@ +.no-print { + display: none !important; +} + @font-face { font-family: 'Arial Plus'; src: local('Arial'); diff --git a/js/menu.js b/js/menu.js index da5e68c8..4fcbf7d8 100644 --- a/js/menu.js +++ b/js/menu.js @@ -1069,11 +1069,11 @@ function sortByClauseNumber(clause1, clause2) { function makeLinkToId(id) { let hash = '#' + id; - if (typeof idToSection === 'undefined' || !idToSection[id]) { - return hash; + if (typeof isMultipage !== 'undefined' && isMultipage && idToSection[id]) { + let targetSec = idToSection[id]; + return (targetSec === 'index' ? './' : targetSec + '.html') + hash; } - let targetSec = idToSection[id]; - return (targetSec === 'index' ? './' : targetSec + '.html') + hash; + return hash; } function doShortcut(e) { @@ -1088,23 +1088,8 @@ function doShortcut(e) { 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; - } + if (e.key === 'm' && typeof toggleMultipage !== 'undefined') { + toggleMultipage(); } else if (e.key === 'u') { document.documentElement.classList.toggle('show-uc-annotations'); } else if (e.key === 'e') { diff --git a/js/multipage.js b/js/multipage.js index a813ea76..d68baa68 100644 --- a/js/multipage.js +++ b/js/multipage.js @@ -1,4 +1,14 @@ 'use strict'; + +let parseSpecPath = url => { + let pathParts = url.pathname.split('/'); + let isMultipage = pathParts[pathParts.length - 2] === 'multipage'; + let pathPrefixEnd = isMultipage ? -2 : pathParts.findLastIndex(part => part !== '') + 1; + let pathPrefix = pathParts.slice(0, pathPrefixEnd).join('/'); + return { pathParts, pathPrefix, isMultipage }; +}; + +// initialize globals let idToSection = Object.create(null); for (let [section, ids] of Object.entries(multipageMap)) { for (let id of ids) { @@ -7,13 +17,91 @@ for (let [section, ids] of Object.entries(multipageMap)) { } } } -if (location.hash) { - let targetSec = idToSection[location.hash.substring(1)]; - if (targetSec != null) { - let match = location.pathname.match(/([^/]+)\.html?$/); - if ((match != null && match[1] !== targetSec) || location.pathname.endsWith('/multipage/')) { - window.navigating = true; - location = (targetSec === 'index' ? './' : targetSec + '.html') + location.hash; +let { pathParts, pathPrefix, isMultipage } = parseSpecPath(location); +let activeSec = isMultipage ? pathParts[pathParts.length - 1].replace(/\.html$/, '') : undefined; +let activeSecHash = + activeSec && idToSection['sec-' + activeSec] != null ? '#sec-' + activeSec : undefined; +let storage = typeof localStorage !== 'undefined' ? localStorage : Object.create(null); +let toggleMultipage = () => { + let hash = location.hash; + if (isMultipage) { + location = pathParts.slice(0, -2).join('/') + '/' + (hash || activeSecHash || ''); + } else { + let targetSec = hash ? idToSection[hash.substring(1)] : undefined; + location = 'multipage/' + (targetSec ? targetSec + '.html' : '') + hash; + } +}; + +// redirect to single-page/multi-page per preference +(() => { + // ...except from internal links + let referrer; + try { + referrer = new URL(document.referrer); + } catch (_err) { + // ignore + } + if (referrer && referrer.host === location.host) { + if (parseSpecPath(referrer).pathPrefix === pathPrefix) return; + } + + let resolvedHash = location.hash || activeSecHash || ''; + let targetSec = resolvedHash ? idToSection[resolvedHash.substring(1)] : undefined; + let multipagePreference = storage.multipagePreference; + if (isMultipage && multipagePreference === 'single-page') { + window.navigating = true; + location = pathParts.slice(0, -2).join('/') + '/' + resolvedHash; + } else if ( + isMultipage + ? targetSec != null && (activeSec || 'index') !== targetSec + : multipagePreference === 'multi-page' + ) { + window.navigating = true; + location = 'multipage/' + (targetSec ? targetSec + '.html' : '') + location.hash; + } +})(); + +// enable preference togglers +document.documentElement.dataset.multipagePreference = storage.multipagePreference || ''; +if (typeof localStorage !== 'undefined') { + let enableToggles = () => { + for (let el of document.querySelectorAll('[disabled][data-multipage-preference]')) { + el.disabled = false; } + }; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', enableToggles); + } else { + enableToggles(); } + document.addEventListener('click', e => { + if (!(e.target instanceof HTMLElement)) { + return; + } + let target = e.target; + let toggler = target.closest('[data-multipage-preference]'); + if (target.isContentEditable || !toggler || toggler === document.documentElement) { + return; + } + switch (target.nodeName.toLowerCase()) { + case 'textarea': + case 'select': + return; + case 'input': { + let isCheckable = target.type === 'checkbox' || target.type === 'radio'; + if (toggler !== target || !isCheckable || !target.checked) { + return; + } + } + } + let multipagePreference = toggler.dataset.multipagePreference; + if (multipagePreference !== (storage.multipagePreference || '')) { + storage.multipagePreference = multipagePreference; + document.documentElement.dataset.multipagePreference = multipagePreference; + if (multipagePreference === (isMultipage ? 'single-page' : 'multi-page')) { + toggleMultipage(); + } + } + e.preventDefault(); + }); } diff --git a/spec/index.html b/spec/index.html index 3e9fbf92..74e9ea3e 100644 --- a/spec/index.html +++ b/spec/index.html @@ -91,6 +91,12 @@
Ecmarkup requires CSS styles and other assets. By default all assets are inlined into the document. You can override this by setting assets to “none” (for example if you want to manually link to external assets) or “external”. When using “external” the default directory for assets is `assets` in the same directory as the output file, but you can override this with `--assets-dir`.
+Multi-page builds support a sticky preference for accessing the resulting document as a single page or as multiple pages. It can be set by embedding elements (such as <a> or <button>) with attribute data-multipage-preference set to either an empty string (for the default lack of preference), "single-page", or "multi-page". When such an element is clicked, the corresponding preference is written into localStorage and a navigation is triggered if necessary. From that point forward, loading any page directly or from an external link will respect that preference and redirect as necessary.
There are a large number of features in Ecmarkup. Detailed documentation can be found in later sections. This section provides a high-level overview of what capabilities are available and when to use them.
diff --git a/src/Spec.ts b/src/Spec.ts index cf675b84..55c9631c 100644 --- a/src/Spec.ts +++ b/src/Spec.ts @@ -687,8 +687,7 @@ export default class Spec { this.doc.body.insertBefore(ele, this.doc.body.firstChild); } - const jsContents = - (await concatJs(sdoJs, tocJs)) + `\n;let usesMultipage = ${!!this.opts.multipage}`; + const jsContents = await concatJs(sdoJs, tocJs); const jsSha = sha(jsContents); await this.buildAssets(jsContents, jsSha); @@ -1010,8 +1009,6 @@ export default class Spec { htmlEle = src.substring(0, src.length - '