diff --git a/assets/build/build.js b/assets/build/build.js index fbc52d82e..4e8f22435 100644 --- a/assets/build/build.js +++ b/assets/build/build.js @@ -71,6 +71,22 @@ Promise.all(formatters.map(async ({formatter, ...options}) => { } }) + // Load html templates. + build.onLoad({ + filter: /\.html$/ + }, async ({ path: filename }) => { + try { + const source = await fs.readFile(filename, 'utf-8') + // Remove newlines and leading whitespace. + // Shouldn't have any effect on content. + const compressed = source.replace(/\n\s*/g, '') + const contents = `export default ${JSON.stringify(compressed)}` + return { contents } + } catch (error) { + return { errors: [{ text: error.message }] } + } + }) + // Generate docs with new assets (watch mode only). if (watchMode) { build.onEnd(async result => { diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css index 5ab44a722..fb16f2a04 100644 --- a/assets/css/sidebar.css +++ b/assets/css/sidebar.css @@ -127,7 +127,7 @@ padding: 0; } -.sidebar .sidebar-list-nav li button { +.sidebar .sidebar-list-nav button { background: none; border: 0; border-radius: 0; @@ -142,29 +142,30 @@ transition: all 150ms; } -.sidebar .sidebar-list-nav li:is(.selected) button { - background-color: var(--sidebarBackground); - border-top: var(--navTabBorderWidth) solid var(--sidebarLanguageAccentBar); -} - -.sidebar .sidebar-list-nav li:not(.selected) button { +.sidebar .sidebar-list-nav button { border-top: var(--navTabBorderWidth) solid var(--sidebarHeader); } -.sidebar .sidebar-list-nav li:is(:hover):not(.selected) button { +.sidebar .sidebar-list-nav button:not([aria-selected]):hover { background-color: var(--sidebarInactiveItemMarker); border-top: var(--navTabBorderWidth) solid var(--sidebarInactiveItemBorder); color: var(--sidebarAccentMain); transition: all 150ms; } +.sidebar .sidebar-list-nav button[aria-selected] { + background-color: var(--sidebarBackground); + border-top: var(--navTabBorderWidth) solid var(--sidebarLanguageAccentBar); +} + .sidebar .sidebar-tabpanel { flex: 1 1 0.01%; overflow-y: auto; overscroll-behavior: contain; position: relative; -webkit-overflow-scrolling: touch; - margin-top: 12px; + padding-top: 12px; + scroll-padding-top: 40px; } .sidebar .full-list { @@ -174,127 +175,106 @@ } .sidebar .full-list :is(li, a) { + display: block; overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; } .sidebar .full-list li { padding: 0; - margin-right: 30px; line-height: 27px; - white-space: nowrap; } -.sidebar .full-list li.docs { - margin-right: 0; +.sidebar .full-list li.group { + text-transform: uppercase; + font-weight: bold; + font-size: 0.8em; + margin: 1.5em 0 0; + line-height: 1.8em; + color: var(--sidebarSubheadings); + padding-left: 15px; } -.sidebar .full-list li.open > ul { - display: block; - margin-left: 10px; +.sidebar .full-list li.nesting-context { + font-weight: bold; + font-size: 0.9em; + line-height: 1.8em; + color: var(--sidebarSubheadings); + margin-top: 10px; + padding-left: 15px; +} + +.sidebar .full-list a { + margin-right: 30px; + padding: 3px 0 3px 15px; + color: var(--sidebarItem); +} + +.sidebar .full-list a[aria-selected] { + color: var(--sidebarActiveItem); } -.sidebar .full-list li a.expand + button.icon-expand { +.sidebar .full-list button { appearance: none; background-color: transparent; border: 0; padding: 0; cursor: pointer; color: inherit; - margin-right: 10px; + width: 20px; + text-align: center; font-size: calc(1.2 * var(--sidebarFontSize)); line-height: var(--sidebarLineHeight); position: absolute; - display: flex; - right: 0; - transform: translateY(calc(-100% - 4px)); + display: block; + right: 10px; + transform: translateY(-100%); +} + +.sidebar .full-list a[aria-selected] + button { + color: var(--sidebarActiveItem); } -.sidebar .full-list li a + button.icon-expand:after { +.sidebar .full-list button:after { font-family: remixicon; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -} - -.sidebar .full-list li a.expand + button.icon-expand:after { content: var(--icon-arrow-down-s); } -.sidebar .full-list li.open > a.expand + button.icon-expand:after { +.sidebar .full-list button[aria-expanded=true]:after { content: var(--icon-arrow-up-s); } -.sidebar .full-list li.docs > a + button.icon-expand { - margin-right: 12px; - font-size: var(--sidebarFontSize); - line-height: var(--sidebarFontSize); - transform: translateY(calc(-100% - 5px)); -} - -.sidebar .full-list li.docs > a + button.icon-expand:after { - content: var(--icon-add); -} - -.sidebar .full-list li.docs.open > a + button.icon-expand:after { - content: var(--icon-subtract); -} - -.sidebar .full-list li.nesting-context { - font-weight: bold; - font-size: 0.9em; - line-height: 1.8em; - color: var(--sidebarSubheadings); - margin-top: 10px; - padding-left: 15px; +.sidebar .full-list ul { + display: none; + margin: 10px 0 10px 10px; + padding: 0; } -.sidebar .full-list li.group { - text-transform: uppercase; - font-weight: bold; - font-size: 0.8em; - margin: 1.5em 0 0; - line-height: 1.8em; - color: var(--sidebarSubheadings); - padding-left: 15px; +.sidebar .full-list button[aria-expanded=true] + ul { + display: block; } -.sidebar .full-list li a { - padding: 3px 0 3px 15px; - color: var(--sidebarItem); -} +/* Level 1 */ .sidebar .full-list > li > a { - display: block; - width: 100%; height: 27px; line-height: var(--sidebarLineHeight); } -.sidebar .full-list li .current-section > a { - color: var(--sidebarActiveItem); -} - -.sidebar .full-list li .current-section > a + button.icon-expand { - color: var(--sidebarActiveItem); -} - .sidebar .full-list > li > a:hover { border-left: 3px solid var(--sidebarLanguageAccentBar); padding-left: 12px; } -.sidebar .full-list > li.current-page > a { - color: var(--sidebarActiveItem); +.sidebar .full-list > li > a[aria-selected] { border-left: 3px solid var(--sidebarLanguageAccentBar); padding-left: 12px; } -.sidebar .full-list > li.current-page > a:after, -.sidebar .full-list > li.current-page { - color: var(--sidebarActiveItem); -} - .sidebar .full-list > li:last-child { margin-bottom: 30px; } @@ -303,84 +283,65 @@ margin-top: 0; } -.sidebar .full-list ul { - display: none; - margin: 10px 15px; - margin-right: 0; - padding: 0; +/* Level 2 */ + +.sidebar .full-list > li > ul > li > a:hover:before { + content: "\2022"; + position: absolute; + margin-left: -15px; + color: var(--sidebarActiveItem); } +/* Level 2+ */ + .sidebar .full-list ul li { - font-weight: 300; line-height: var(--sidebarFontSize); padding: 0 8px; - margin-right: 0; - color: var(--sidebarAccentMain); } -:root:not(.apple-os) .sidebar .full-list ul li { - font-weight: 400; /* Non-Apple OSes render small light type too thinly */ +.sidebar .full-list ul a { + padding-left: 15px; + height: 24px; } -.sidebar .full-list ul li.current-hash { - color: var(--sidebarActiveItem); +.sidebar .full-list ul button { + font-size: var(--sidebarFontSize); } -.sidebar .full-list ul li.current-hash > a { - color: var(--sidebarActiveItem); +.sidebar .full-list ul button:after { + content: var(--icon-add); } -.sidebar .full-list ul li.current-hash > a:before, -.sidebar .full-list > li > ul > li > a:hover:before { - content: "\2022"; - position: absolute; - margin-left: -15px; - color: var(--sidebarActiveItem); +.sidebar .full-list ul button[aria-expanded=true]:after { + content: var(--icon-subtract); } -.sidebar .full-list ul li a { - padding-left: 15px; - display: block; - width: 100%; - height: 24px; -} +/* Level 3+ */ -.sidebar .full-list ul li ul { - display: none; - margin: 9px 20px; - margin-right: 0; +.sidebar .full-list ul ul { + margin: 9px 0 9px 10px; } -.sidebar .full-list ul li ul li { - margin-right: 0; +.sidebar .full-list ul ul li { height: 20px; color: var(--sidebarAccentMain); } -.sidebar .full-list ul li ul li a { +.sidebar .full-list ul ul a { border-left: 1px solid var(--sidebarInactiveItemMarker); padding: 0 10px; height: 20px; } -.sidebar .full-list ul li ul li.current-hash > a:before { - content: none; -} - -.sidebar .full-list ul li ul li > a:hover { +.sidebar .full-list ul ul a:hover { border-color: var(--sidebarLanguageAccentBar); } -.sidebar .full-list ul li ul li.current-hash > a { +.sidebar .full-list ul ul a[aria-selected] { color: var(--sidebarActiveItem); border-color: var(--sidebarLanguageAccentBar); } -.sidebar .full-list ul li ul li.current-hash > a { - color: var(--sidebarActiveItem); - margin-left: 0; -} - .sidebar ::-webkit-scrollbar { width: 14px; } diff --git a/assets/js/copy-button.js b/assets/js/copy-button.js index c4d622383..cee3ecc7d 100644 --- a/assets/js/copy-button.js +++ b/assets/js/copy-button.js @@ -1,23 +1,22 @@ import { qsAll } from './helpers' +import buttonHtml from './handlebars/templates/copy-button.html' -const template = document.createElement('div') -template.innerHTML = '' -const buttonTemplate = template.firstChild +/** @type {HTMLButtonElement} */ +let buttonTemplate /** * Initializes copy buttons. */ export function initialize () { - if ('clipboard' in navigator) { - addCopyButtons() - } -} + if (!('clipboard' in navigator)) return -/** - * Find pre tags, add copy buttons, copy content on click. - */ -function addCopyButtons () { qsAll('pre:has(> code:first-child):not(:has(.copy-button))').forEach(pre => { + if (!buttonTemplate) { + const div = document.createElement('div') + div.innerHTML = buttonHtml + buttonTemplate = div.firstChild + } + const button = buttonTemplate.cloneNode(true) pre.appendChild(button) diff --git a/assets/js/entry/html.js b/assets/js/entry/html.js index 3b22276d3..498554c4c 100644 --- a/assets/js/entry/html.js +++ b/assets/js/entry/html.js @@ -1,17 +1,14 @@ import { onDocumentReady } from '../helpers' import { initialize as initTabsets } from '../tabsets' import { initialize as initContent } from '../content' -import { initialize as initSidebarDrawer, update as updateSidebarDrawer } from '../sidebar/sidebar-drawer' -import { initialize as initSidebarContent, update as updateSidebarContent } from '../sidebar/sidebar-list' +import { initialize as initSidebarDrawer } from '../sidebar/sidebar-drawer' import { initialize as initSearch } from '../search-bar' import { initialize as initVersions } from '../sidebar/sidebar-version-select' import { initialize as initSearchPage } from '../search-page' import { initialize as initTheme } from '../theme' import { initialize as initMakeup } from '../makeup' -import { initialize as initModal } from '../modal' import { initialize as initKeyboardShortcuts } from '../keyboard-shortcuts' import { initialize as initQuickSwitch } from '../quick-switch' -import { initialize as initToast } from '../toast' import { initialize as initTooltips } from '../tooltips/tooltips' import { initialize as initHintsPage } from '../tooltips/hint-page' import { initialize as initCopyButton } from '../copy-button' @@ -58,8 +55,6 @@ onDocumentReady(() => { initTooltips() initCopyButton() - updateSidebarDrawer() - updateSidebarContent() initSearch() initSearchPage() initSettings() @@ -71,13 +66,10 @@ onDocumentReady(() => { } initVersions() - initModal() initKeyboardShortcuts() initQuickSwitch() - initToast() initSidebarDrawer() - initSidebarContent() initSearch() initSearchPage() initSettings() diff --git a/assets/js/handlebars/helpers.js b/assets/js/handlebars/helpers.js index 17c355e4b..11bb0b748 100644 --- a/assets/js/handlebars/helpers.js +++ b/assets/js/handlebars/helpers.js @@ -1,45 +1,5 @@ import * as Handlebars from 'handlebars/runtime' -Handlebars.registerHelper('groupChanged', function (context, nodeGroup, options) { - const group = nodeGroup || '' - if (context.group !== group) { - // reset the nesting context for the #nestingChanged block helper - delete context.nestedContext - context.group = group - return options.fn(this) - } -}) - -Handlebars.registerHelper('nestingChanged', function (context, node, options) { - // context.nestedContext is also reset each time a new group - // is encountered (the value is reset within the #groupChanged - // block helper) - if (node.nested_context && node.nested_context !== context.nestedContext) { - context.nestedContext = node.nested_context - - if (context.lastModuleSeenInGroup !== node.nested_context) { - return options.fn(this) - } - } else { - // track the most recently seen module - // prevents emitting a duplicate entry for nesting when - // the nesting prefix matches an existing module - context.lastModuleSeenInGroup = node.title - } -}) - -Handlebars.registerHelper('showSections', function (node, options) { - if (node.sections.length > 0) { - return options.fn(this) - } -}) - -Handlebars.registerHelper('showSummary', function (node, options) { - if (node.nodeGroups) { - return options.fn(this) - } -}) - Handlebars.registerHelper('isArray', function (entry, options) { if (Array.isArray(entry)) { return options.fn(this) @@ -55,21 +15,3 @@ Handlebars.registerHelper('isNonEmptyArray', function (entry, options) { return options.inverse(this) } }) - -Handlebars.registerHelper('isEmptyArray', function (entry, options) { - if (Array.isArray(entry) && entry.length === 0) { - return options.fn(this) - } else { - return options.inverse(this) - } -}) - -Handlebars.registerHelper('isLocal', function (nodeId, options) { - const pathSuffix = window.location.pathname.split('/').pop() - - if (pathSuffix === nodeId + '.html' || pathSuffix === nodeId) { - return options.fn(this) - } else { - return options.inverse(this) - } -}) diff --git a/assets/js/handlebars/templates/copy-button.html b/assets/js/handlebars/templates/copy-button.html new file mode 100644 index 000000000..8196242df --- /dev/null +++ b/assets/js/handlebars/templates/copy-button.html @@ -0,0 +1,7 @@ + diff --git a/assets/js/handlebars/templates/modal-layout.handlebars b/assets/js/handlebars/templates/modal-layout.html similarity index 100% rename from assets/js/handlebars/templates/modal-layout.handlebars rename to assets/js/handlebars/templates/modal-layout.html diff --git a/assets/js/handlebars/templates/quick-switch-modal-body.handlebars b/assets/js/handlebars/templates/quick-switch-modal-body.html similarity index 100% rename from assets/js/handlebars/templates/quick-switch-modal-body.handlebars rename to assets/js/handlebars/templates/quick-switch-modal-body.html diff --git a/assets/js/handlebars/templates/quick-switch-results.handlebars b/assets/js/handlebars/templates/quick-switch-results.handlebars deleted file mode 100644 index 671a503cd..000000000 --- a/assets/js/handlebars/templates/quick-switch-results.handlebars +++ /dev/null @@ -1,5 +0,0 @@ -{{#each results}} -
- {{name}} -
-{{/each}} diff --git a/assets/js/handlebars/templates/sidebar-items.handlebars b/assets/js/handlebars/templates/sidebar-items.handlebars deleted file mode 100644 index ce2364244..000000000 --- a/assets/js/handlebars/templates/sidebar-items.handlebars +++ /dev/null @@ -1,76 +0,0 @@ -{{#each nodes as |node nodeId|}} - {{#groupChanged ../this node.group}} -
  • - {{node.group}} -
  • - {{/groupChanged}} - - {{#nestingChanged ../this node}} - - {{/nestingChanged}} - -
  • - - {{#if node.nested_title}} - {{{node.nested_title}}} - {{else}} - {{{node.title}}} - {{/if}} - - - {{#isEmptyArray node.headers}} - {{else}} - - {{/isEmptyArray}} - - {{#isArray node.headers}} - {{#isNonEmptyArray node.headers}} - - {{/isNonEmptyArray}} - {{else}} - - {{/isArray}} -
  • -{{/each}} diff --git a/assets/js/helpers.js b/assets/js/helpers.js index 748799991..c56c2db8a 100644 --- a/assets/js/helpers.js +++ b/assets/js/helpers.js @@ -46,28 +46,6 @@ export function getCurrentPageSidebarType () { return document.getElementById('main').dataset.type } -/** - * Looks up a nested node having the specified anchor - * and returns the corresponding category. - * - * @param {Array} nodes A list of sidebar nodes. - * @param {String|null} anchor The anchor to look for. - * @returns {String} The relevant node group key, like 'functions', 'types', etc. - */ -export function findSidebarCategory (nodes, anchor) { - if (!nodes) return - - for (const node of nodes) { - const nodeGroup = node.nodeGroups && node.nodeGroups.find(nodeGroup => - nodeGroup.nodes.some(subnode => subnode.anchor === anchor) - ) - - if (nodeGroup) return nodeGroup.key - } - - return null -} - /** * Finds an element by a URL hash (e.g. a function section). * @@ -221,3 +199,24 @@ export function isAppleOS () { // Set in inline_html.js return document.documentElement.classList.contains('apple-os') } + +/** + * Create element from tag, attributes and children. + * + * @param {string} tagName + * @param {Record} attributes + * @param {(HTMLElement | string)[]} [children] + * @returns {HTMLElement} + */ +export function el (tagName, attributes, children) { + const element = document.createElement(tagName) + for (const key in attributes) { + if (attributes[key] != null) { + element.setAttribute(key, attributes[key]) + } + } + if (children) { + element.replaceChildren(...children) + } + return element +} diff --git a/assets/js/keyboard-shortcuts.js b/assets/js/keyboard-shortcuts.js index ba5a6157b..a70e8a088 100644 --- a/assets/js/keyboard-shortcuts.js +++ b/assets/js/keyboard-shortcuts.js @@ -64,10 +64,6 @@ const state = { * listing all available options. */ export function initialize () { - addEventListeners() -} - -function addEventListeners () { document.addEventListener('keydown', handleKeyDown) document.addEventListener('keyup', handleKeyUp) } diff --git a/assets/js/modal.js b/assets/js/modal.js index 1bc62dc29..f4889aa14 100644 --- a/assets/js/modal.js +++ b/assets/js/modal.js @@ -1,45 +1,38 @@ import { qs } from './helpers' -import modalLayoutTemplate from './handlebars/templates/modal-layout.handlebars' +import modalLayoutHtml from './handlebars/templates/modal-layout.html' -const MODAL_SELECTOR = '.modal' -const MODAL_CLOSE_BUTTON_SELECTOR = '.modal .modal-close' -const MODAL_TITLE_SELECTOR = '.modal .modal-title' -const MODAL_BODY_SELECTOR = '.modal .modal-body' const FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' -const state = { - prevFocus: null, - lastFocus: null, - ignoreFocusChanges: false -} -/** - * Initializes modal layout. - */ -export function initialize () { - renderModal() -} +// State + +/** @type {HTMLDivElement | null} */ +let modal = null +/** @type {HTMLElement | null} */ +let prevFocus = null +/** @type {HTMLElement | null} */ +let lastFocus = null +let ignoreFocusChanges = false /** * Adds the modal to DOM, initially it's hidden. */ function renderModal () { - const modalLayoutHtml = modalLayoutTemplate() + if (modal) return + document.body.insertAdjacentHTML('beforeend', modalLayoutHtml) + modal = qs('.modal') - qs(MODAL_SELECTOR).addEventListener('keydown', event => { + modal.addEventListener('keydown', event => { if (event.key === 'Escape') { closeModal() } }) - qs(MODAL_CLOSE_BUTTON_SELECTOR).addEventListener('click', event => { - closeModal() - }) + modal.querySelector('.modal-close').addEventListener('click', closeModal) - qs(MODAL_SELECTOR).addEventListener('click', event => { - const classList = event.target.classList + modal.addEventListener('click', event => { // if we clicked on the modal overlay/parent but not the modal content - if (classList.contains('modal')) { + if (event.target === modal) { closeModal() } }) @@ -49,63 +42,57 @@ function renderModal () { * Trap focus in modal * Only called on open modals */ -function trapFocus (event) { - if (state.ignoreFocusChanges) return - const modal = qs(MODAL_SELECTOR) +function handleFocus (event) { + if (ignoreFocusChanges) return + if (modal.contains(event.target)) { - state.lastFocus = event.target + lastFocus = event.target } else { - state.ignoreFocusChanges = true - const firstFocusable = firstFocusableDescendant(modal) - if (state.lastFocus === firstFocusable) { - lastFocusableDescendant(modal).focus() + ignoreFocusChanges = true + const focusable = modal.querySelectorAll(FOCUSABLE_SELECTOR) + if (lastFocus === focusable[0]) { + // Focus last + focusable[focusable.length - 1].focus() } else { - firstFocusable.focus() + // Focus first + focusable[0].focus() } - state.ignoreFocusChanges = false - state.lastFocus = document.activeElement + ignoreFocusChanges = false + lastFocus = document.activeElement } } -function firstFocusableDescendant (element) { - return element.querySelector(FOCUSABLE_SELECTOR) -} - -function lastFocusableDescendant (element) { - const elements = element.querySelectorAll(FOCUSABLE_SELECTOR) - return elements[elements.length - 1] -} - /** * Shows modal with the given content. * - * @param {{ title: String, body: String }} attrs + * @param {{ title: string, body: string }} attrs */ export function openModal ({ title, body }) { - state.prevFocus = document.activeElement - document.addEventListener('focus', trapFocus, true) + renderModal() + prevFocus = document.activeElement + document.addEventListener('focus', handleFocus, true) - qs(MODAL_TITLE_SELECTOR).innerHTML = title - qs(MODAL_BODY_SELECTOR).innerHTML = body + modal.querySelector('.modal-title').innerHTML = title + modal.querySelector('.modal-body').innerHTML = body - qs(MODAL_SELECTOR).classList.add('shown') - qs(MODAL_SELECTOR).focus() + modal.classList.add('shown') + modal.focus() } /** * Closes the modal. */ export function closeModal () { - qs(MODAL_SELECTOR).classList.remove('shown') + modal?.classList.remove('shown') - document.addEventListener('focus', trapFocus, true) - state.prevFocus && state.prevFocus.focus() - state.prevFocus = null + document.removeEventListener('focus', handleFocus, true) + prevFocus?.focus() + prevFocus = null } /** * Checks whether a modal is open. */ export function isModalOpen () { - return qs(MODAL_SELECTOR).classList.contains('shown') + return Boolean(modal?.classList.contains('shown')) } diff --git a/assets/js/quick-switch.js b/assets/js/quick-switch.js index 8871c8e14..cbc45e43f 100644 --- a/assets/js/quick-switch.js +++ b/assets/js/quick-switch.js @@ -1,7 +1,6 @@ -import { debounce, qs, qsAll } from './helpers' +import { debounce, el, qs, qsAll } from './helpers' import { openModal } from './modal' -import quickSwitchModalBodyTemplate from './handlebars/templates/quick-switch-modal-body.handlebars' -import quickSwitchResultsTemplate from './handlebars/templates/quick-switch-results.handlebars' +import quickSwitchModalBodyHtml from './handlebars/templates/quick-switch-modal-body.html' const HEX_DOCS_ENDPOINT = 'https://hexdocs.pm/%%' const OTP_DOCS_ENDPOINT = 'https://www.erlang.org/doc/apps/%%' @@ -9,7 +8,6 @@ const HEX_SEARCH_ENDPOINT = 'https://hex.pm/api/packages?search=name:%%*' const QUICK_SWITCH_LINK_SELECTOR = '.display-quick-switch' const QUICK_SWITCH_INPUT_SELECTOR = '#quick-switch-input' const QUICK_SWITCH_RESULTS_SELECTOR = '#quick-switch-results' -const QUICK_SWITCH_RESULT_SELECTOR = '.quick-switch-result' const DEBOUNCE_KEYPRESS_TIMEOUT = 300 const NUMBER_OF_SUGGESTIONS = 9 @@ -74,14 +72,8 @@ const state = { * Initializes the quick switch modal. */ export function initialize () { - addEventListeners() -} - -function addEventListeners () { qsAll(QUICK_SWITCH_LINK_SELECTOR).forEach(element => { - element.addEventListener('click', event => { - openQuickSwitchModal() - }) + element.addEventListener('click', openQuickSwitchModal) }) } @@ -117,12 +109,11 @@ function handleInput (event) { export function openQuickSwitchModal () { openModal({ title: 'Go to package docs', - body: quickSwitchModalBodyTemplate() + body: quickSwitchModalBodyHtml }) - qs(QUICK_SWITCH_INPUT_SELECTOR).focus() - const quickSwitchInput = qs(QUICK_SWITCH_INPUT_SELECTOR) + quickSwitchInput.focus() quickSwitchInput.addEventListener('keydown', handleKeyDown) quickSwitchInput.addEventListener('input', handleInput) @@ -179,24 +170,18 @@ function queryForAutocomplete (packageSlug) { // Only render results if the search string is still long enough const currentTerm = qs(QUICK_SWITCH_INPUT_SELECTOR).value if (currentTerm.length >= MIN_SEARCH_LENGTH) { - renderResults({ results: state.autocompleteResults }) + renderResults(state.autocompleteResults) } } }) } -function renderResults ({ results }) { - const resultsContainer = qs(QUICK_SWITCH_RESULTS_SELECTOR) - const resultsHtml = quickSwitchResultsTemplate({ results }) - resultsContainer.innerHTML = resultsHtml - - qsAll(QUICK_SWITCH_RESULT_SELECTOR).forEach(result => { - result.addEventListener('click', event => { - const index = result.getAttribute('data-index') - const selectedResult = state.autocompleteResults[index] - navigateToAppDocs(selectedResult.name) - }) - }) +function renderResults (results) { + qs(QUICK_SWITCH_RESULTS_SELECTOR).replaceChildren(...results.map(({name}, index) => { + const resultEl = el('div', {class: 'quick-switch-result', 'data-index': index}, [name]) + resultEl.addEventListener('click', () => navigateToAppDocs(name)) + return resultEl + })) } /** diff --git a/assets/js/sidebar/sidebar-drawer.js b/assets/js/sidebar/sidebar-drawer.js index 254c0c7f3..65961d7c5 100644 --- a/assets/js/sidebar/sidebar-drawer.js +++ b/assets/js/sidebar/sidebar-drawer.js @@ -1,6 +1,7 @@ import throttle from 'lodash.throttle' import { qs } from '../helpers' import { SIDEBAR_CLASS_OPEN, SIDEBAR_CLASS_TRANSITION, SIDEBAR_PREF_CLOSED, SIDEBAR_PREF_OPEN, SIDEBAR_STATE_KEY, SIDEBAR_WIDTH_KEY, SMALL_SCREEN_BREAKPOINT } from './constants' +import { initialize as initializeList } from './sidebar-list' const ANIMATION_DURATION = 300 @@ -10,6 +11,8 @@ const SIDEBAR_TOGGLE_SELECTOR = '.sidebar-toggle' export function initialize () { update() + window.addEventListener('swup:page:view', update) + qs(SIDEBAR_TOGGLE_SELECTOR).addEventListener('click', toggleSidebar) // Clicks outside small screen open sidebar should close it. @@ -45,6 +48,7 @@ export function initialize () { export function update () { const pref = sessionStorage.getItem(SIDEBAR_STATE_KEY) const open = pref !== SIDEBAR_PREF_CLOSED && !isScreenSmall() + if (open) initializeList() updateSidebar(open) } diff --git a/assets/js/sidebar/sidebar-list.js b/assets/js/sidebar/sidebar-list.js index 397cf5a57..d7a6a7172 100644 --- a/assets/js/sidebar/sidebar-list.js +++ b/assets/js/sidebar/sidebar-list.js @@ -1,237 +1,249 @@ -import { qs, getCurrentPageSidebarType, getLocationHash, findSidebarCategory } from '../helpers' +import { el, getCurrentPageSidebarType, qs, qsAll } from '../helpers' import { getSidebarNodes } from '../globals' -import sidebarItemsTemplate from '../handlebars/templates/sidebar-items.handlebars' - -const SIDEBAR_TYPE = { - search: 'search', - extras: 'extras', - modules: 'modules', - tasks: 'tasks' -} - -const SIDEBAR_TAB_TYPES = [SIDEBAR_TYPE.extras, SIDEBAR_TYPE.modules, SIDEBAR_TYPE.tasks] -const sidebarNodeListSelector = type => `#${type}-full-list` +let init = false export function initialize () { - update() - addEventListeners() -} + if (init) return + init = true -export function update () { - SIDEBAR_TAB_TYPES.forEach(type => { - renderSidebarNodeList(getSidebarNodes(), type) - }) + const sidebarList = document.getElementById('sidebar-list-nav') - markActiveSidebarTab(getCurrentPageSidebarType()) - markCurrentHashInSidebar() - scrollNodeListToCurrentCategory() -} + if (!sidebarList) return -/** - * Fill the sidebar with links to different nodes - * - * This function replaces an empty unordered list with an - * unordered list full of links to the different tasks, exceptions - * and modules mentioned in the documentation. - * - * @param {Object} nodesByType - Container of tasks, exceptions and modules. - * @param {String} type - Filter of nodes, by default the type of the current page. - */ -function renderSidebarNodeList (nodesByType, type) { - const nodes = nodesByType[type] || [] - - // Render the list - const nodeList = qs(sidebarNodeListSelector(type)) - if (!nodeList) { return } - const listContentHtml = sidebarItemsTemplate({ nodes, group: '' }) - nodeList.innerHTML = listContentHtml - - // Removes the "expand" class from links belonging to single-level sections - nodeList.querySelectorAll('ul').forEach(list => { - if (list.innerHTML.trim() === '') { - const emptyExpand = list.previousElementSibling - if (emptyExpand.classList.contains('expand')) { - emptyExpand.classList.remove('expand') - } - list.remove() - } - }) + const defaultTab = getCurrentPageSidebarType() + const tabs = { + extras: sidebarList.dataset.extras || 'Pages', + modules: 'Modules', + tasks: 'Mix Tasks' + } - // Register event listeners - nodeList.querySelectorAll('li a + button').forEach(button => { - button.addEventListener('click', event => { - const target = event.target - const listItem = target.closest('li') - toggleListItem(listItem) - }) - }) + Object.entries(tabs).forEach(([type, titleHtml]) => { + const nodes = getSidebarNodes()[type] - nodeList.querySelectorAll('li a').forEach(anchor => { - anchor.addEventListener('click', event => { - const target = event.target - const listItem = target.closest('li') - const previousSection = nodeList.querySelector('.current-section') + if (!nodes?.length) return - // Clear the previous current section - if (previousSection) { - clearCurrentSectionElement(previousSection) + const tabId = `${type}-list-tab-button` + const tabpanelId = `${type}-tab-panel` + const selected = type === defaultTab + + const tab = el('button', { + id: tabId, + role: 'tab', + tabindex: selected ? 0 : -1, + 'aria-selected': selected || undefined, + 'aria-controls': tabpanelId + }) + tab.innerHTML = titleHtml + tab.addEventListener('keydown', handleTabKeydown) + tab.addEventListener('click', handleTabClick) + sidebarList.appendChild(el('li', {}, [tab])) + + const nodeList = el('ul', {class: 'full-list'}) + nodeList.addEventListener('click', handleNodeListClick) + + const tabpanel = el('div', { + id: tabpanelId, + class: 'sidebar-tabpanel', + role: 'tabpanel', + 'aria-labelledby': tabId, + hidden: selected ? undefined : '' + }, [nodeList]) + document.getElementById('sidebar').appendChild(tabpanel) + + let group = '' + let nestedContext + let lastModule + nodeList.replaceChildren(...nodes.flatMap(node => { + const items = [] + const hasHeaders = Array.isArray(node.headers) + const translate = hasHeaders ? undefined : 'no' + + // Group header. + if (node.group !== group) { + items.push(el('li', {class: 'group', translate}, [node.group])) + group = node.group + nestedContext = undefined } - if (anchor.matches('.expand') && - (anchor.pathname === window.location.pathname || - anchor.pathname === window.location.pathname + '.html')) { - openListItem(listItem) + // Nesting context. + if (node.nested_context && node.nested_context !== nestedContext) { + nestedContext = node.nested_context + if (lastModule !== nestedContext) { + items.push(el('li', {class: 'nesting-context', translate: 'no', 'aria-hidden': true}, [nestedContext])) + } + } else { + lastModule = node.title } - }) + + items.push(el('li', {}, [ + el('a', {href: `${node.id}.html`, translate}, [node.nested_title || node.title]), + ...childList(`node-${node.id}-headers`, + hasHeaders + ? renderHeaders(node) + : renderSectionsAndGroups(node) + ) + ])) + + return items + })) }) + + window.addEventListener('hashchange', markCurrentHashInSidebar) + window.addEventListener('swup:page:view', markCurrentHashInSidebar) + + markCurrentHashInSidebar() + // Triggers layout, defer. + requestAnimationFrame(scrollNodeListToCurrentCategory) } -function openListItem (listItem) { - listItem.classList.add('open') - listItem.querySelector('button[aria-controls]').setAttribute('aria-expanded', 'true') +/** + * @param {string} id + * @param {HTMLElement[]} childItems + */ +function childList (id, childItems) { + if (!childItems.length) return [] + + return [ + el('button', {'aria-label': 'expand', 'aria-expanded': false, 'aria-controls': id}), + el('ul', {id}, childItems) + ] } -function closeListItem (listItem) { - listItem.classList.remove('open') - listItem.querySelector('button[aria-controls]').setAttribute('aria-expanded', 'false') +function renderHeaders (node) { + return node.headers + .map(({id, anchor}) => + el('li', {}, [ + el('a', {href: `${node.id}.html#${anchor}`}, [id]) + ]) + ) } -function toggleListItem (listItem) { - if (listItem.classList.contains('open')) { - closeListItem(listItem) - } else { - openListItem(listItem) +function renderSectionsAndGroups (node) { + const items = [] + + if (node.sections?.length) { + items.push(el('li', {}, [ + el('a', {href: `${node.id}.html#content`}, ['Sections']), + ...childList(`${node.id}-sections-list`, + node.sections + .map(({id, anchor}) => + el('li', {}, [ + el('a', {href: `${node.id}.html#${anchor}`}, [id]) + ]) + ) + ) + ])) } -} -function markElementAsCurrentSection (section) { - section.classList.add('current-section') - section.querySelector('a').setAttribute('aria-current', 'true') -} + if (node.nodeGroups) { + items.push(el('li', {}, [ + el('a', {href: `${node.id}.html#summary`}, ['Summary']) + ])) + + items.push(...node.nodeGroups.map(({key, name, nodes}) => + el('li', {}, [ + el('a', {href: `${node.id}.html#${key}`}, [name]), + ...childList(`node-${node.id}-group-${key}-list`, + nodes + .map(({anchor, title, id}) => + el('li', {}, [ + el('a', {href: `${node.id}.html#${anchor}`, title, translate: 'no'}, [id]) + ]) + ) + ) + ]) + )) + } -function clearCurrentSectionElement (section) { - section.classList.remove('current-section') - section.querySelector('a').setAttribute('aria-current', 'false') + return items } -function markElementAsCurrentHash (listItem) { - listItem.classList.add('current-hash') - listItem.querySelector('a').setAttribute('aria-current', 'true') -} +/** @param {HTMLButtonElement} */ +function activateTab (next) { + const prev = document.getElementById('sidebar-list-nav').querySelector('[aria-selected]') -function clearCurrentHashElement (listItem) { - listItem.classList.remove('current-hash') - listItem.querySelector('a').setAttribute('aria-current', 'false') -} + if (prev === next) return -function markActiveSidebarTab (activeType) { - SIDEBAR_TAB_TYPES.forEach(type => { - const button = qs(`#${type}-list-tab-button`) - if (button) { - const tabpanel = qs(`#${button.getAttribute('aria-controls')}`) - if (type === activeType) { - button.parentElement.classList.add('selected') - button.setAttribute('aria-selected', 'true') - button.setAttribute('tabindex', '0') - tabpanel.removeAttribute('hidden') - } else { - button.parentElement.classList.remove('selected') - button.setAttribute('aria-selected', 'false') - button.setAttribute('tabindex', '-1') - tabpanel.setAttribute('hidden', 'hidden') - } - } - }) + if (prev) { + prev.removeAttribute('aria-selected') + prev.setAttribute('tabindex', '-1') + document.getElementById(prev.getAttribute('aria-controls')).setAttribute('hidden', 'hidden') + } + + next.setAttribute('aria-selected', 'true') + next.setAttribute('tabindex', '0') + document.getElementById(next.getAttribute('aria-controls')).removeAttribute('hidden') } function scrollNodeListToCurrentCategory () { - const nodeList = qs(sidebarNodeListSelector(getCurrentPageSidebarType())) - if (!nodeList) { return } - - const currentPage = nodeList.querySelector('li.current-page') - if (currentPage) { - currentPage.scrollIntoView() - nodeList.scrollTop -= 40 - } + qs('#sidebar [role=tabpanel]:not([hidden]) a[aria-selected]')?.scrollIntoView() } function markCurrentHashInSidebar () { - const hash = getLocationHash() || 'content' + const sidebar = document.getElementById('sidebar') + const {pathname, hash} = window.location - const sidebarNodes = getSidebarNodes() - const nodes = sidebarNodes[getCurrentPageSidebarType()] || [] - const category = findSidebarCategory(nodes, hash) - const nodeList = qs(sidebarNodeListSelector(getCurrentPageSidebarType())) - if (!nodeList) { return } + // All sidebar links are relative and end in .html. + const page = pathname.split('/').pop().replace(/\.html$/, '') + '.html' - const categoryEl = nodeList.querySelector(`li.current-page a.expand[href$="#${category}"]`) - if (categoryEl) { - openListItem(categoryEl.closest('li')) - } + // Try find exact link with hash, fall back to page. + const current = sidebar.querySelector(`li a[href="${page + hash}"]`) || sidebar.querySelector(`li a[href="${page}"]`) - const hashEl = nodeList.querySelector(`li.current-page a[href$="#${hash}"]`) - if (hashEl) { - const deflist = hashEl.closest('ul') - if (deflist.classList.contains('deflist')) { - markElementAsCurrentSection(deflist.closest('li')) - } - markElementAsCurrentHash(hashEl.closest('li')) - } -} + if (!current) return -function addEventListeners () { - // Bind the navigation links ("Pages", "Modules", "Tasks") - // so that they render a list of all relevant nodes when clicked. - SIDEBAR_TAB_TYPES.forEach(type => { - const button = qs(`#${type}-list-tab-button`) - if (button) { - button.addEventListener('click', event => { - markActiveSidebarTab(type) - scrollNodeListToCurrentCategory() - }) - } + // Unset previous. + sidebar.querySelectorAll('li a[aria-selected]').forEach(element => { + element.removeAttribute('aria-selected') }) - // provide left/right arrow navigation for tablist, as required by ARIA authoring practices guide - const tabList = qs('#sidebar-list-nav') - tabList.addEventListener('keydown', (e) => { - if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') { return } - - // SIDEBAR_TAB_TYPES cannot be used here as it always contains all possible types, not only types that the specific project has - const tabTypes = Array.from(tabList.querySelectorAll('[role="tab"]')).map(tab => tab.dataset.type) - // getCurrentPageSidebarType() cannot be used here as it's assigned once, on page render - const currentTabType = tabList.querySelector('[role="tab"][aria-selected="true"]').dataset.type - - if (e.key === 'ArrowRight') { - let nextTabTypeIndex = tabTypes.indexOf(currentTabType) + 1 - if (nextTabTypeIndex >= tabTypes.length) { - nextTabTypeIndex = 0 + // Walk up parents, updating link, button and tab attributes. + let element = current.parentElement + while (element) { + if (element.tagName === 'LI') { + const link = element.firstChild + link.setAttribute('aria-selected', link.getAttribute('href') === page ? 'page' : 'true') + const button = link.nextSibling + if (button?.tagName === 'BUTTON') { + button.setAttribute('aria-expanded', true) } - - const nextType = tabTypes[nextTabTypeIndex] - markActiveSidebarTab(nextType) - qs(`#${nextType}-list-tab-button`).focus() - } else if (e.key === 'ArrowLeft') { - let previousTabTypeIndex = tabTypes.indexOf(currentTabType) - 1 - if (previousTabTypeIndex < 0) { - previousTabTypeIndex = tabTypes.length - 1 + } else if (element.role === 'tabpanel') { + if (element.hasAttribute('hidden')) { + activateTab(document.getElementById(element.getAttribute('aria-labelledby'))) } - - const previousType = tabTypes[previousTabTypeIndex] - markActiveSidebarTab(previousType) - qs(`#${previousType}-list-tab-button`).focus() + break } - }) + element = element.parentElement + } +} + +/** + * Provide left/right arrow navigation for tablist, as required by ARIA authoring practices guide. + * + * @param {KeyboardEvent} + **/ +function handleTabKeydown (event) { + if (!['ArrowRight', 'ArrowLeft'].includes(event.key)) { return } + + const tabs = Array.from(qsAll('#sidebar-list-nav [role="tab"]')) + const currentIndex = tabs.indexOf(event.currentTarget) + const nextIndex = currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + const nextTab = tabs.at(nextIndex % tabs.length) + + activateTab(nextTab) + nextTab.focus() +} + +/** @param {MouseEvent} */ +function handleTabClick (event) { + activateTab(event.currentTarget) + scrollNodeListToCurrentCategory() +} - // Keep .current-hash item in sync with the hash, regardless how the change takes place - window.addEventListener('hashchange', event => { - const nodeList = qs(sidebarNodeListSelector(getCurrentPageSidebarType())) - if (!nodeList) { return } +/** @param {MouseEvent} */ +function handleNodeListClick (event) { + const target = event.target - const currentListItem = nodeList.querySelector('li.current-page li.current-hash') - if (currentListItem) { - clearCurrentHashElement(currentListItem) - } - markCurrentHashInSidebar() - }) + if (target.tagName === 'BUTTON') { + target.setAttribute('aria-expanded', target.getAttribute('aria-expanded') === 'false') + } } diff --git a/assets/js/tabsets.js b/assets/js/tabsets.js index fdbd22a36..082f08da9 100644 --- a/assets/js/tabsets.js +++ b/assets/js/tabsets.js @@ -1,3 +1,5 @@ +import { el } from './helpers' + const CONTENT_CONTAINER_ID = 'content' const TABSET_OPEN_COMMENT = 'tabs-open' const TABSET_CLOSE_COMMENT = 'tabs-close' @@ -5,11 +7,7 @@ const TABPANEL_HEADING_NODENAME = 'H3' const TABSET_CONTAINER_CLASS = 'tabset' export function initialize () { - // Done in read and mutate parts to avoid layout thrashing. - // Reading inner text requires layout so we want to read - // all headings before we start mutating the DOM. - - /** @type {[Node, [string, HTMLElement[]][]][]} */ + /** @type {[Node, [NodeList, HTMLElement[]][]][]} */ const sets = [] /** @type {Node[]} */ const toRemove = [] @@ -36,7 +34,9 @@ export function initialize () { if (node.nodeName === TABPANEL_HEADING_NODENAME) { // Tab heading. tabContent = [] - set.push([node.innerText, tabContent]) + // Extract heading text nodes (faster than using .textContent which requires layout). + const headingContent = node.querySelector('.text')?.childNodes || node.childNodes + set.push([headingContent, tabContent]) toRemove.push(node) } else if (node.nodeName === '#comment' && node.nodeValue.trim() === TABSET_CLOSE_COMMENT) { // Closer comment. @@ -49,7 +49,6 @@ export function initialize () { } } - // Now we can mutate DOM. sets.forEach(([opener, set], setIndex) => { const tabset = el('div', { class: TABSET_CONTAINER_CLASS @@ -62,7 +61,7 @@ export function initialize () { }) tabset.appendChild(tablist) - set.forEach(([text, content], index) => { + set.forEach(([headingContent, content], index) => { const selected = index === 0 const tabId = `tab-${setIndex}-${index}` const tabPanelId = `tabpanel-${setIndex}-${index}` @@ -74,8 +73,7 @@ export function initialize () { tabindex: selected ? 0 : -1, 'aria-selected': selected, 'aria-controls': tabPanelId - }) - tab.innerText = text + }, headingContent) tab.addEventListener('click', handleTabClick) tab.addEventListener('keydown', handleTabKeydown) tablist.appendChild(tab) @@ -87,8 +85,7 @@ export function initialize () { hidden: !selected ? '' : undefined, tabindex: selected ? 0 : -1, 'aria-labelledby': tabId - }) - tabPanel.replaceChildren(...content) + }, content) tabset.appendChild(tabPanel) }) }) @@ -98,21 +95,6 @@ export function initialize () { }) } -/** - * @param {string} tagName - * @param {Record} attributes - * @returns {HTMLElement} - */ -function el (tagName, attributes) { - const element = document.createElement(tagName) - for (const key in attributes) { - if (attributes[key] != null) { - element.setAttribute(key, attributes[key]) - } - } - return element -} - /** @param {MouseEvent} event */ function handleTabClick (event) { activateTab(event.currentTarget) diff --git a/assets/js/toast.js b/assets/js/toast.js index 24b15a0db..cc4948be5 100644 --- a/assets/js/toast.js +++ b/assets/js/toast.js @@ -1,16 +1,17 @@ +let init = false let toastTimer = null let toast = null -export function initialize () { - toast = document.getElementById('toast') - - toast.addEventListener('click', (event) => { - clearTimeout(toastTimer) - event.target.classList.remove('show') - }) -} - export function showToast (message) { + if (!init) { + init = true + toast = document.getElementById('toast') + toast?.addEventListener('click', () => { + clearTimeout(toastTimer) + toast.classList.remove('show') + }) + } + if (toast) { clearTimeout(toastTimer) toast.innerText = message diff --git a/assets/js/tooltips/tooltips.js b/assets/js/tooltips/tooltips.js index 3652d3ec7..6657432f4 100644 --- a/assets/js/tooltips/tooltips.js +++ b/assets/js/tooltips/tooltips.js @@ -40,8 +40,6 @@ const state = { * Initializes tooltips handling. */ export function initialize () { - qs(CONTENT_INNER_SELECTOR).insertAdjacentHTML('beforeend', TOOLTIP_HTML) - qsAll(TOOLTIP_ACTIVATORS_SELECTOR).forEach(element => { if (!linkElementEligibleForTooltip(element)) { return } @@ -99,7 +97,12 @@ function renderTooltip (hint) { hint }) - qs(TOOLTIP_BODY_SELECTOR).innerHTML = tooltipBodyHtml + let tooltipBody = qs(TOOLTIP_BODY_SELECTOR) + if (!tooltipBody) { + qs(CONTENT_INNER_SELECTOR).insertAdjacentHTML('beforeend', TOOLTIP_HTML) + tooltipBody = qs(TOOLTIP_BODY_SELECTOR) + } + tooltipBody.innerHTML = tooltipBodyHtml updateTooltipPosition() @@ -113,7 +116,7 @@ function handleHoverEnd () { clearTimeout(state.hoverDelayTimeout) cancelHintFetchingIfAny() state.currentLinkElement = null - qs(TOOLTIP_SELECTOR).classList.remove(TOOLTIP_SHOWN_CLASS) + qs(TOOLTIP_SELECTOR)?.classList.remove(TOOLTIP_SHOWN_CLASS) } /** diff --git a/assets/test/helpers.spec.js b/assets/test/helpers.spec.js index 2170f509f..ff70f223e 100644 --- a/assets/test/helpers.spec.js +++ b/assets/test/helpers.spec.js @@ -1,4 +1,4 @@ -import { escapeRegexModifiers, findSidebarCategory } from '../js/helpers' +import { escapeRegexModifiers } from '../js/helpers' describe('helpers', () => { describe('escapeRegexModifiers', () => { @@ -6,23 +6,4 @@ describe('helpers', () => { expect(escapeRegexModifiers('hello-world')).to.be.equal('hello\\-world') }) }) - - describe('findSidebarCategory', () => { - it('finds the correct category', () => { - const nodes = [{ - nodeGroups: [ - {key: 'callbacks', nodes: [{anchor: 'hello'}]}, - {key: 'functions', nodes: [{anchor: 'world'}]} - ] - }, { - nodeGroups: [ - {key: 'callbacks', nodes: [{anchor: 'one'}]}, - {key: 'examples', nodes: [{anchor: 'two'}]} - ] - }] - - expect(findSidebarCategory(nodes, 'world')).to.be.eql('functions') - expect(findSidebarCategory(nodes, 'something')).to.be.eql(null) - }) - }) }) diff --git a/lib/ex_doc/formatter/html.ex b/lib/ex_doc/formatter/html.ex index 1f07edef0..4f9d56d6c 100644 --- a/lib/ex_doc/formatter/html.ex +++ b/lib/ex_doc/formatter/html.ex @@ -42,12 +42,12 @@ defmodule ExDoc.Formatter.HTML do search_data ++ static_files ++ generate_sidebar_items(nodes_map, extras, config) ++ - generate_extras(nodes_map, extras, config) ++ + generate_extras(extras, config) ++ generate_logo(@assets_dir, config) ++ - generate_search(nodes_map, config) ++ - generate_not_found(nodes_map, config) ++ - generate_list(nodes_map.modules, nodes_map, config) ++ - generate_list(nodes_map.tasks, nodes_map, config) ++ + generate_search(config) ++ + generate_not_found(config) ++ + generate_list(nodes_map.modules, config) ++ + generate_list(nodes_map.tasks, config) ++ generate_redirects(config, ".html") generate_build(Enum.sort(all_files), build) @@ -166,18 +166,18 @@ defmodule ExDoc.Formatter.HTML do File.write!(build, entries) end - defp generate_not_found(nodes_map, config) do + defp generate_not_found(config) do filename = "404.html" config = set_canonical_url(config, filename) - content = Templates.not_found_template(config, nodes_map) + content = Templates.not_found_template(config) File.write!("#{config.output}/#{filename}", content) [filename] end - defp generate_search(nodes_map, config) do + defp generate_search(config) do filename = "search.html" config = set_canonical_url(config, filename) - content = Templates.search_template(config, nodes_map) + content = Templates.search_template(config) File.write!("#{config.output}/#{filename}", content) [filename] end @@ -203,7 +203,7 @@ defmodule ExDoc.Formatter.HTML do |> binary_part(0, 8) end - defp generate_extras(nodes_map, extras, config) do + defp generate_extras(extras, config) do generated_extras = extras |> with_prev_next() @@ -218,7 +218,7 @@ defmodule ExDoc.Formatter.HTML do } extension = node.source_path && Path.extname(node.source_path) - html = Templates.extra_template(config, node, extra_type(extension), nodes_map, refs) + html = Templates.extra_template(config, node, extra_type(extension), refs) if File.regular?(output) do Utils.warn("file #{Path.relative_to_cwd(output)} already exists", []) @@ -530,16 +530,16 @@ defmodule ExDoc.Formatter.HTML do Enum.filter(nodes, &(&1.type == type)) end - defp generate_list(nodes, nodes_map, config) do + defp generate_list(nodes, config) do nodes - |> Task.async_stream(&generate_module_page(&1, nodes_map, config), timeout: :infinity) + |> Task.async_stream(&generate_module_page(&1, config), timeout: :infinity) |> Enum.map(&elem(&1, 1)) end - defp generate_module_page(module_node, nodes_map, config) do + defp generate_module_page(module_node, config) do filename = "#{module_node.id}.html" config = set_canonical_url(config, filename) - content = Templates.module_page(module_node, nodes_map, config) + content = Templates.module_page(module_node, config) File.write!("#{config.output}/#{filename}", content) filename end diff --git a/lib/ex_doc/formatter/html/templates.ex b/lib/ex_doc/formatter/html/templates.ex index 9e7d81da6..a4ab93a9a 100644 --- a/lib/ex_doc/formatter/html/templates.ex +++ b/lib/ex_doc/formatter/html/templates.ex @@ -15,9 +15,9 @@ defmodule ExDoc.Formatter.HTML.Templates do @doc """ Generate content from the module template for a given `node` """ - def module_page(module_node, nodes_map, config) do + def module_page(module_node, config) do summary = module_summary(module_node) - module_template(config, module_node, summary, nodes_map) + module_template(config, module_node, summary) end @doc """ @@ -291,13 +291,13 @@ defmodule ExDoc.Formatter.HTML.Templates do detail_template: [:node, :module], footer_template: [:config, :node], head_template: [:config, :title, :noindex], - module_template: [:config, :module, :summary, :nodes_map], - not_found_template: [:config, :nodes_map], + module_template: [:config, :module, :summary], + not_found_template: [:config], api_reference_entry_template: [:module_node], api_reference_template: [:nodes_map], - extra_template: [:config, :node, :type, :nodes_map, :refs], - search_template: [:config, :nodes_map], - sidebar_template: [:config, :type, :nodes_map], + extra_template: [:config, :node, :type, :refs], + search_template: [:config], + sidebar_template: [:config, :type], summary_template: [:name, :nodes], redirect_template: [:config, :redirect_to] ] diff --git a/lib/ex_doc/formatter/html/templates/extra_template.eex b/lib/ex_doc/formatter/html/templates/extra_template.eex index 1b835f034..43f9a287c 100644 --- a/lib/ex_doc/formatter/html/templates/extra_template.eex +++ b/lib/ex_doc/formatter/html/templates/extra_template.eex @@ -1,5 +1,5 @@ <%= head_template(config, node.title, false) %> -<%= sidebar_template(config, type, nodes_map) %> +<%= sidebar_template(config, type) %>
    diff --git a/lib/ex_doc/formatter/html/templates/module_template.eex b/lib/ex_doc/formatter/html/templates/module_template.eex index 3c2394e37..e14af45bf 100644 --- a/lib/ex_doc/formatter/html/templates/module_template.eex +++ b/lib/ex_doc/formatter/html/templates/module_template.eex @@ -1,5 +1,5 @@ <%= head_template(config, module.title, false) %> -<%= sidebar_template(config, module.type, nodes_map) %> +<%= sidebar_template(config, module.type) %>
    diff --git a/lib/ex_doc/formatter/html/templates/not_found_template.eex b/lib/ex_doc/formatter/html/templates/not_found_template.eex index e16bc2de1..d4503d1f6 100644 --- a/lib/ex_doc/formatter/html/templates/not_found_template.eex +++ b/lib/ex_doc/formatter/html/templates/not_found_template.eex @@ -1,5 +1,5 @@ <%= head_template(config, "404", true) %> -<%= sidebar_template(config, :extra, nodes_map) %> +<%= sidebar_template(config, :extra) %>

    Page not found diff --git a/lib/ex_doc/formatter/html/templates/search_template.eex b/lib/ex_doc/formatter/html/templates/search_template.eex index 3f471adf2..732b3b5fd 100644 --- a/lib/ex_doc/formatter/html/templates/search_template.eex +++ b/lib/ex_doc/formatter/html/templates/search_template.eex @@ -1,5 +1,5 @@ <%= head_template(config, "Search", true) %> -<%= sidebar_template(config, :search, nodes_map) %> +<%= sidebar_template(config, :search) %>

    - +
    - - - - <%= if nodes_map.modules != [] do %> - - <% end %> - - <%= if nodes_map.tasks != [] do %> - - <% end %> diff --git a/test/ex_doc/formatter/html/templates_test.exs b/test/ex_doc/formatter/html/templates_test.exs index 970e8431a..7cc749bee 100644 --- a/test/ex_doc/formatter/html/templates_test.exs +++ b/test/ex_doc/formatter/html/templates_test.exs @@ -6,8 +6,6 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do @moduletag :tmp_dir - @empty_nodes_map %{modules: [], exceptions: [], protocols: [], tasks: []} - defp source_url do "https://github.com/elixir-lang/elixir" end @@ -33,7 +31,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do config = doc_config(context, config) {mods, []} = ExDoc.Retriever.docs_from_modules(names, config) [mod | _] = HTML.render_all(mods, [], ".html", config, []) - Templates.module_page(mod, @empty_nodes_map, config) + Templates.module_page(mod, config) end setup %{tmp_dir: tmp_dir} do @@ -202,7 +200,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do describe "sidebar" do test "text links to homepage_url when set", context do - content = Templates.sidebar_template(doc_config(context), :extra, @empty_nodes_map) + content = Templates.sidebar_template(doc_config(context), :extra) assert content =~ ~r""" @@ -221,7 +219,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do main: "hello" } - content = Templates.sidebar_template(config, :extra, @empty_nodes_map) + content = Templates.sidebar_template(config, :extra) assert content =~ ~r""" @@ -233,27 +231,6 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do """ end - test "enables nav link when module type have at least one element", context do - names = [CompiledWithDocs, CompiledWithDocs.Nested] - modules = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) - - content = - Templates.sidebar_template(doc_config(context), :extra, %{ - modules: modules, - exceptions: [], - tasks: [] - }) - - assert content =~ - ~r{
  • [\s\n]*[\s\n]*
  • } - - assert content =~ - ~r{} - - refute content =~ ~r{id="tasks-list-tab-button"} - refute content =~ ~r{id="tasks-full-list"} - end - test "display built with footer by proglang option", context do content = Templates.footer_template(doc_config(context, proglang: :erlang), nil)