diff options
-rw-r--r-- | .eslintignore | 1 | ||||
-rw-r--r-- | doc/api_assets/README.md | 4 | ||||
-rw-r--r-- | doc/api_assets/api.js | 141 | ||||
-rw-r--r-- | doc/api_assets/style.css | 47 | ||||
-rw-r--r-- | doc/template.html | 38 |
5 files changed, 190 insertions, 41 deletions
diff --git a/.eslintignore b/.eslintignore index 8ab4750abd1..5941496e1a6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,4 +7,5 @@ tools/icu tools/lint-md/lint-md.mjs benchmark/tmp doc/**/*.js +!doc/api_assets/*.js !.eslintrc.js diff --git a/doc/api_assets/README.md b/doc/api_assets/README.md index 07262bba4ce..e2c1d90cd09 100644 --- a/doc/api_assets/README.md +++ b/doc/api_assets/README.md @@ -1,5 +1,9 @@ # API Reference Document Assets +## api.js + +The main script for API reference documents. + ## hljs.css The syntax theme for code snippets in API reference documents. diff --git a/doc/api_assets/api.js b/doc/api_assets/api.js new file mode 100644 index 00000000000..4304a254600 --- /dev/null +++ b/doc/api_assets/api.js @@ -0,0 +1,141 @@ +'use strict'; + +{ + function setupTheme() { + const kCustomPreference = 'customDarkTheme'; + const userSettings = sessionStorage.getItem(kCustomPreference); + const themeToggleButton = document.getElementById('theme-toggle-btn'); + + if (userSettings === null && window.matchMedia) { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + + if ('onchange' in mq) { + function mqChangeListener(e) { + document.documentElement.classList.toggle('dark-mode', e.matches); + } + mq.addEventListener('change', mqChangeListener); + if (themeToggleButton) { + themeToggleButton.addEventListener('click', function() { + mq.removeEventListener('change', mqChangeListener); + }, { once: true }); + } + } + + if (mq.matches) { + document.documentElement.classList.add('dark-mode'); + } + } else if (userSettings === 'true') { + document.documentElement.classList.add('dark-mode'); + } + + if (themeToggleButton) { + themeToggleButton.hidden = false; + themeToggleButton.addEventListener('click', function() { + sessionStorage.setItem( + kCustomPreference, + document.documentElement.classList.toggle('dark-mode') + ); + }); + } + } + + function setupPickers() { + function closeAllPickers() { + for (const picker of pickers) { + picker.parentNode.classList.remove('expanded'); + } + + window.removeEventListener('click', closeAllPickers); + window.removeEventListener('keydown', onKeyDown); + } + + function onKeyDown(e) { + if (e.key === 'Escape') { + closeAllPickers(); + } + } + + const pickers = document.querySelectorAll('.picker-header > a'); + + for (const picker of pickers) { + const parentNode = picker.parentNode; + + picker.addEventListener('click', (e) => { + e.preventDefault(); + + /* + closeAllPickers as window event trigger already closed all the pickers, + if it already closed there is nothing else to do here + */ + if (parentNode.classList.contains('expanded')) { + return; + } + + /* + In the next frame reopen the picker if needed and also setup events + to close pickers if needed. + */ + + requestAnimationFrame(() => { + parentNode.classList.add('expanded'); + window.addEventListener('click', closeAllPickers); + window.addEventListener('keydown', onKeyDown); + }); + }); + } + } + + function setupStickyHeaders() { + const header = document.querySelector('.header'); + let ignoreNextIntersection = false; + + new IntersectionObserver( + ([e]) => { + const currentStatus = header.classList.contains('is-pinned'); + const newStatus = e.intersectionRatio < 1; + + // Same status, do nothing + if (currentStatus === newStatus) { + return; + } else if (ignoreNextIntersection) { + ignoreNextIntersection = false; + return; + } + + /* + To avoid flickering, ignore the next changes event that is triggered + as the visible elements in the header change once we pin it. + + The timer is reset anyway after few milliseconds. + */ + ignoreNextIntersection = true; + setTimeout(() => { + ignoreNextIntersection = false; + }, 50); + + header.classList.toggle('is-pinned', newStatus); + }, + { threshold: [1] } + ).observe(header); + } + + function bootstrap() { + // Check if we have JavaScript support + document.documentElement.classList.add('has-js'); + + // Restore user mode preferences + setupTheme(); + + // Handle pickers with click/taps rather than hovers + setupPickers(); + + // Track when the header is in sticky position + setupStickyHeaders(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); + } else { + bootstrap(); + } +} diff --git a/doc/api_assets/style.css b/doc/api_assets/style.css index fa12c02ce7d..14302edc061 100644 --- a/doc/api_assets/style.css +++ b/doc/api_assets/style.css @@ -189,19 +189,23 @@ li.picker-header .expanded-arrow { display: none; } -li.picker-header:hover .collapsed-arrow { +li.picker-header.expanded .collapsed-arrow, +:root:not(.has-js) li.picker-header:hover .collapsed-arrow { display: none; } -li.picker-header:hover .expanded-arrow { +li.picker-header.expanded .expanded-arrow, +:root:not(.has-js) li.picker-header:hover .expanded-arrow { display: inline-block; } -li.picker-header:hover > a { +li.picker-header.expanded > a, +:root:not(.has-js) li.picker-header:hover > a { border-radius: 2px 2px 0 0; } -li.picker-header:hover > .picker { +li.picker-header.expanded > .picker, +:root:not(.has-js) li.picker-header:hover > .picker { display: block; z-index: 1; } @@ -807,13 +811,38 @@ kbd { background-color: var(--color-fill-app); } -@media not screen, (max-height: 1000px) { +@media not screen, (max-width: 600px) { .header { position: relative; top: 0; } } +@media not screen, (max-height: 1000px) { + :root:not(.has-js) .header { + position: relative; + top: 0; + } +} + +.header .pinned-header { + display: none; + margin-right: 0.4rem; + font-weight: 700; +} + +.header.is-pinned .header-container { + display: none; +} + +.header.is-pinned .pinned-header { + display: inline; +} + +.header.is-pinned #gtoc { + margin: 0; +} + .header-container { display: flex; align-items: center; @@ -845,6 +874,14 @@ kbd { padding-right: 0; } + .header #gtoc > ul > li.pinned-header { + display: none; + } + + .header.is-pinned #gtoc > ul > li.pinned-header { + display: inline; + } + #gtoc > ul > li.gtoc-picker-header { display: none; } diff --git a/doc/template.html b/doc/template.html index 89dd2fbeac9..86ba3c9581e 100644 --- a/doc/template.html +++ b/doc/template.html @@ -9,6 +9,7 @@ <link rel="stylesheet" href="assets/style.css"> <link rel="stylesheet" href="assets/hljs.css"> <link rel="canonical" href="https://nodejs.org/api/__FILENAME__.html"> + <script async defer src="assets/api.js" type="text/javascript"></script> </head> <body class="alt apidoc" id="api-section-__FILENAME__"> <div id="content" class="clearfix"> @@ -39,6 +40,7 @@ </div> <div id="gtoc"> <ul> + <li class="pinned-header">Node.js __VERSION__</li> __TOC_PICKER__ __GTOC_PICKER__ __ALTDOCS__ @@ -73,41 +75,5 @@ </div> </div> </div> - <script> - 'use strict'; - { - const kCustomPreference = 'customDarkTheme'; - const userSettings = sessionStorage.getItem(kCustomPreference); - const themeToggleButton = document.getElementById('theme-toggle-btn'); - if (userSettings === null && window.matchMedia) { - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - if ('onchange' in mq) { - function mqChangeListener(e) { - document.documentElement.classList.toggle('dark-mode', e.matches); - } - mq.addEventListener('change', mqChangeListener); - if (themeToggleButton) { - themeToggleButton.addEventListener('click', function() { - mq.removeEventListener('change', mqChangeListener); - }, { once: true }); - } - } - if (mq.matches) { - document.documentElement.classList.add('dark-mode'); - } - } else if (userSettings === 'true') { - document.documentElement.classList.add('dark-mode'); - } - if (themeToggleButton) { - themeToggleButton.hidden = false; - themeToggleButton.addEventListener('click', function() { - sessionStorage.setItem( - kCustomPreference, - document.documentElement.classList.toggle('dark-mode') - ); - }); - } - } - </script> </body> </html> |