diff --git a/app/assets/stylesheets/partials/_results.scss b/app/assets/stylesheets/partials/_results.scss index db73f250..0d9ea95f 100644 --- a/app/assets/stylesheets/partials/_results.scss +++ b/app/assets/stylesheets/partials/_results.scss @@ -156,9 +156,9 @@ // -------------------------------------- .result.use, .result.primo { - padding: 48px 0; + padding: 32px 0 32px; margin: 0; - border-bottom: 1px solid $color-gray-400; + border-bottom: 1px solid $color-gray-200; border-top: none; &:first-child { diff --git a/app/javascript/source_tabs.js b/app/javascript/source_tabs.js index 67f0a411..65813048 100644 --- a/app/javascript/source_tabs.js +++ b/app/javascript/source_tabs.js @@ -3,93 +3,194 @@ // Source: https://css-tricks.com/container-adapting-tabs-with-more-button/ // =========================================================================== -// Store references to relevant selectors -const container = document.querySelector('#tabs') -const primary = container.querySelector('.primary') -const primaryItems = container.querySelectorAll('.primary > li:not(.-more)') - -// Add a class to turn off graceful degradation style -container.classList.add('has-js') - -// insert "more" button and duplicate the original tab bar items -primary.insertAdjacentHTML('beforeend', ` -
  • - - -
  • -`) -const secondary = container.querySelector('.-secondary') -const secondaryItems = secondary.querySelectorAll('li') -const allItems = container.querySelectorAll('li') -const moreLi = primary.querySelector('.-more') -const moreBtn = moreLi.querySelector('button') - -// When the more button is clicked, toggle classes to indicate the secondary menu is open -moreBtn.addEventListener('click', (e) => { - e.preventDefault() - container.classList.toggle('--show-secondary') - moreBtn.setAttribute('aria-expanded', container.classList.contains('--show-secondary')) -}) - -// Maximum number of tabs to show in the primary tab bar at once -const MAX_TABS = 10 - -// adapt tabs -const doAdapt = () => { - - // reveal all items for the calculation - allItems.forEach((item) => { - item.classList.remove('--hidden') +// Initialize tab bar functionality +const initTabs = () => { + // Store references to relevant selectors + const container = document.querySelector('#tabs') + if (!container) return // Exit if tabs element doesn't exist + if (container.classList.contains('has-js')) return // Already initialized + + const primary = container.querySelector('.primary') + + // Add a class to turn off graceful degradation style + container.classList.add('has-js') + + // insert "more" button and duplicate the original tab bar items + primary.insertAdjacentHTML('beforeend', ` +
  • + + +
  • + `) + const secondary = container.querySelector('.-secondary') + const allItems = container.querySelectorAll('li') + const moreLi = primary.querySelector('.-more') + const moreBtn = moreLi.querySelector('button') + + // Store original DOM order by index for each li + Array.from(primary.querySelectorAll('li:not(.-more)')).forEach((li, index) => { + li.dataset.originalIndex = index + }) + Array.from(secondary.querySelectorAll('li:not(.-more)')).forEach((li, index) => { + li.dataset.originalIndex = index }) - // hide items that won't fit in the Primary tab bar, or exceed MAX_TABS - // once a tab is hidden, all subsequent tabs are also hidden to preserve order - let stopWidth = moreBtn.offsetWidth - let hiddenItems = [] - let shouldHide = false - const primaryWidth = primary.offsetWidth - primaryItems.forEach((item, i) => { - if(!shouldHide && i < MAX_TABS && primaryWidth >= stopWidth + item.offsetWidth) { - stopWidth += item.offsetWidth - } else { - shouldHide = true - item.classList.add('--hidden') - hiddenItems.push(i) - } + // When the more button is clicked, toggle classes to indicate the secondary menu is open + moreBtn.addEventListener('click', (e) => { + e.preventDefault() + container.classList.toggle('--show-secondary') + moreBtn.setAttribute('aria-expanded', container.classList.contains('--show-secondary')) }) - - // toggle the visibility of More button and items in Secondary menu - if(!hiddenItems.length) { - moreLi.classList.add('--hidden') - container.classList.remove('--show-secondary') - moreBtn.setAttribute('aria-expanded', false) - } - else { - secondaryItems.forEach((item, i) => { - if(!hiddenItems.includes(i)) { + + // Maximum number of tabs to show in the primary tab bar at once + const MAX_TABS = 10 + + // adapt tabs + const doAdapt = () => { + + // reveal all items for the calculation + allItems.forEach((item) => { + item.classList.remove('--hidden') + }) + + // Get primary items in current DOM order (re-query each time, direct children only) + const currentPrimaryItems = Array.from(primary.querySelectorAll(':scope > li:not(.-more)')) + + // hide items that won't fit in the Primary tab bar, or exceed MAX_TABS + // once a tab is hidden, all subsequent tabs are also hidden to preserve order + let stopWidth = moreBtn.offsetWidth + let hiddenItems = [] + let shouldHide = false + const primaryWidth = primary.offsetWidth + currentPrimaryItems.forEach((item, i) => { + if(!shouldHide && i < MAX_TABS && primaryWidth >= stopWidth + item.offsetWidth) { + stopWidth += item.offsetWidth + } else { + shouldHide = true item.classList.add('--hidden') + hiddenItems.push(i) } }) - } -} + + // toggle the visibility of More button and items in Secondary menu + if(!hiddenItems.length) { + moreLi.classList.add('--hidden') + container.classList.remove('--show-secondary') + moreBtn.setAttribute('aria-expanded', false) + } + else { + // Re-query secondary items in current DOM order (direct children only) + const currentSecondaryItems = Array.from(secondary.querySelectorAll(':scope > li:not(.-more)')) + currentSecondaryItems.forEach((item, i) => { + if(!hiddenItems.includes(i)) { + item.classList.add('--hidden') + } + }) + } + + // Handle active hidden tab - move it to position 2 (right after "All") + const activeLink = primary.querySelector('a.active') + if (activeLink) { + const activeLi = activeLink.parentElement -// Adapt the tabs to fit the viewport -doAdapt() // immediately on load -window.addEventListener('resize', doAdapt) // on window resize + if (activeLi.classList.contains('--hidden')) { + const activeIndex = currentPrimaryItems.indexOf(activeLi) -// hide Secondary menu on the outside click -document.addEventListener('click', (e) => { - let el = e.target - while(el) { - if(el === moreBtn) { - return; + // If active tab is not at position 0 (All) or position 1, move it to position 1 + if (activeIndex > 1) { + const liAtPos1 = currentPrimaryItems[1] + + // Swap in primary by moving active before position 1 + primary.insertBefore(activeLi, liAtPos1) + + // Find matching items in secondary by original index + const activeOriginalIndex = parseInt(activeLi.dataset.originalIndex) + const pos1OriginalIndex = parseInt(liAtPos1.dataset.originalIndex) + + const currentSecondaryItems = Array.from(secondary.querySelectorAll(':scope > li:not(.-more)')) + const secondaryActive = currentSecondaryItems.find(li => + parseInt(li.dataset.originalIndex) === activeOriginalIndex + ) + const secondaryPos1 = currentSecondaryItems.find(li => + parseInt(li.dataset.originalIndex) === pos1OriginalIndex + ) + + // Swap in secondary + if (secondaryActive && secondaryPos1) { + secondary.insertBefore(secondaryActive, secondaryPos1) + } + + // Recalculate visibility after swap + const updatedPrimaryItems = Array.from(primary.querySelectorAll(':scope > li:not(.-more)')) + const updatedSecondaryItems = Array.from(secondary.querySelectorAll(':scope > li:not(.-more)')) + + // Clear hidden from all items before recalculating + updatedPrimaryItems.forEach(item => item.classList.remove('--hidden')) + updatedSecondaryItems.forEach(item => item.classList.remove('--hidden')) + moreLi.classList.remove('--hidden') + + // Recalculate which items fit + let newStopWidth = moreBtn.offsetWidth + let newHiddenItems = [] + let newShouldHide = false + + updatedPrimaryItems.forEach((item, i) => { + if (!newShouldHide && i < MAX_TABS && primaryWidth >= newStopWidth + item.offsetWidth) { + newStopWidth += item.offsetWidth + } else { + newShouldHide = true + item.classList.add('--hidden') + newHiddenItems.push(i) + } + }) + + // Update secondary visibility + if (!newHiddenItems.length) { + moreLi.classList.add('--hidden') + container.classList.remove('--show-secondary') + moreBtn.setAttribute('aria-expanded', false) + } else { + moreLi.classList.remove('--hidden') + updatedSecondaryItems.forEach((item, i) => { + if (!newHiddenItems.includes(i)) { + item.classList.add('--hidden') + } + }) + } + } + } } - el = el.parentNode } - container.classList.remove('--show-secondary') - moreBtn.setAttribute('aria-expanded', false) -}) + + // Adapt the tabs to fit the viewport + doAdapt() // immediately on load + window.addEventListener('resize', doAdapt) // on window resize + + // hide Secondary menu on the outside click + document.addEventListener('click', (e) => { + let el = e.target + while(el) { + if(el === moreBtn) { + return; + } + el = el.parentNode + } + container.classList.remove('--show-secondary') + moreBtn.setAttribute('aria-expanded', false) + }) +} + +// Initialize on page load and after Turbo navigates +// turbo:load fires on both initial page load and subsequent Turbo navigations + if (!window.__timdexSourceTabsTurboListenerAdded) { + window.__timdexSourceTabsTurboListenerAdded = true + document.addEventListener('turbo:load', initTabs) + } + +// Run immediately in case turbo:load already fired before this script loaded +// (happens on first search when Turbo Drive loads the page containing this script) +initTabs()