diff --git a/source/css/_common/outline/sidebar/sidebar-nav.styl b/source/css/_common/outline/sidebar/sidebar-nav.styl index 5a49bee61..eabfb8942 100644 --- a/source/css/_common/outline/sidebar/sidebar-nav.styl +++ b/source/css/_common/outline/sidebar/sidebar-nav.styl @@ -1,12 +1,19 @@ // Sidebar Navigation .sidebar-nav { - display: none; + font-size: $font-size-small; + height: 0; margin: 0; - padding-bottom: 20px; + overflow: hidden; padding-left: 0; + pointer-events: none; + transition: $transition-ease; + transition-property: height, visibility; + visibility: hidden; .sidebar-nav-active & { - display: block; + height: "calc(%sem + 1px)" % $line-height-base; + pointer-events: unset; + visibility: unset; } li { @@ -14,7 +21,8 @@ color: $sidebar-nav-color; cursor: pointer; display: inline-block; - font-size: $font-size-small; + transition: $transition-ease; + transition-property: border-bottom-color, color; &.sidebar-nav-overview { margin-left: 10px; @@ -29,30 +37,93 @@ .sidebar-toc-active .sidebar-nav-toc, .sidebar-overview-active .sidebar-nav-overview { border-bottom-color: $sidebar-highlight; color: $sidebar-highlight; + transition-delay: $transition-duration; &:hover { color: $sidebar-highlight; } } -// Need for Sidebar/TOC inner scrolling if content taller then viewport. +// For TOC/Overview scrolling .sidebar-panel-container { + align-items: start; + display: grid; flex: 1; overflow-x: hidden; overflow-y: auto; + padding-top: 0; + transition: padding-top $transition-ease; + + .sidebar-nav-active & { + padding-top: 20px; + } } .sidebar-panel { - display: none; + animation: deactivate-sidebar-panel $transition-duration ease-in-out; + grid-area: 1 / 1; + height: 0; + opacity: 0; + overflow: hidden; + pointer-events: none; + transform: translateY(0); + transition: $transition-ease; + transition-delay: 0s; + transition-property: opacity, transform, visibility; + visibility: hidden; + + // Apply transform to both panels when sidebar nav is active, + // to the TOC panel when switching between Overview and TOC regardless of + // whether the sidebar nav is active + .sidebar-nav-active &, + .sidebar-overview-active &.post-toc-wrap { + transform: translateY(-20px); + } + + // Delay TOC transform transition when switching from TOC to Overview and + // deactivating the sidebar nav at the same time, to prevent the TOC panel + // from moving too fast + // https://github.com/next-theme/hexo-theme-next/pull/323#issuecomment-1420780965 + .sidebar-overview-active:not(.sidebar-nav-active) &.post-toc-wrap { + transition-delay: 0s, $transition-duration, 0s; + } + + .sidebar-overview-active &.site-overview-wrap, + .sidebar-toc-active &.post-toc-wrap { + animation-name: activate-sidebar-panel; + height: auto; + opacity: 1; + pointer-events: unset; + transform: translateY(0); + // The visibility delay is intentionally set to 0s to accommodate + // the visibility change on initial page load. + transition-delay: $transition-duration, $transition-duration, 0s; + visibility: unset; + } + + &.site-overview-wrap { + // Flexbox layout makes it possible to reorder the child + // elements of .site-overview-wrap through the `order` CSS property + flex-column(); + gap: 10px; + justify-content: flex-start; // TODO: Optimize the duplicate with flex-column() + } } -.sidebar-overview-active .site-overview-wrap { - // Flexbox layout makes it possible to reorder the child - // elements of .site-overview-wrap through the `order` CSS property - flex-column(); - gap: 10px; +@keyframes deactivate-sidebar-panel { + from { + height: var(--inactive-panel-height, 0); + } + to { + height: var(--active-panel-height, 0); + } } -.sidebar-toc-active .post-toc-wrap { - display: block; +@keyframes activate-sidebar-panel { + from { + height: var(--inactive-panel-height, auto); + } + to { + height: var(--active-panel-height, auto); + } } diff --git a/source/css/_common/outline/sidebar/sidebar-toc.styl b/source/css/_common/outline/sidebar/sidebar-toc.styl index 18f47292e..253191dc3 100644 --- a/source/css/_common/outline/sidebar/sidebar-toc.styl +++ b/source/css/_common/outline/sidebar/sidebar-toc.styl @@ -5,9 +5,13 @@ if (hexo-config('toc.enable')) { ol { list-style: none; margin: 0; - padding: 0 2px 5px 10px; + padding: 0 2px 0 10px; text-align: left; + > :last-child { + margin-bottom: 5px; + } + > ol { padding-left: 0; } @@ -28,19 +32,21 @@ if (hexo-config('toc.enable')) { } .nav { - .nav-child { - display: hexo-config('toc.expand_all') ? block : none; - } - - .active > .nav-child { - display: block; - } - - .active-current > .nav-child { - display: block; + if (not hexo-config('toc.expand_all')) { + .nav-child { + --height: 0; + height: 0; + opacity: 0; + overflow: hidden; + transition-property: height, opacity, visibility; + transition: $transition-ease; + visibility: hidden; + } - > .nav-item { - display: block; + .active > .nav-child { + height: var(--height, auto); + opacity: 1; + visibility: unset; } } diff --git a/source/css/_variables/base.styl b/source/css/_variables/base.styl index 96515f8cf..e7fb8c359 100644 --- a/source/css/_variables/base.styl +++ b/source/css/_variables/base.styl @@ -24,9 +24,10 @@ $orange = #fc6423; // Transition // -------------------------------------------------- -$transition-ease = .2s ease-in-out; -$transition-ease-in = .2s ease-in; -$transition-ease-out = .2s ease-out; +$transition-duration = .2s; +$transition-ease = $transition-duration ease-in-out; +$transition-ease-in = $transition-duration ease-in; +$transition-ease-out = $transition-duration ease-out; // Scaffolding diff --git a/source/js/pjax.js b/source/js/pjax.js index ec24341d7..f81a6a0b1 100644 --- a/source/js/pjax.js +++ b/source/js/pjax.js @@ -4,11 +4,25 @@ const pjax = new Pjax({ selectors: [ 'head title', 'script[type="application/json"]', - '.main-inner', + // Precede .main-inner to prevent placeholder TOC changes asap '.post-toc-wrap', + '.main-inner', '.languages', '.pjax' ], + switches: { + '.post-toc-wrap': function(oldWrap, newWrap) { + if (newWrap.querySelector('.post-toc')) { + Pjax.switches.outerHTML.call(this, oldWrap, newWrap); + } else { + const curTOC = oldWrap.querySelector('.post-toc'); + if (curTOC) { + curTOC.classList.add('placeholder-toc'); + } + this.onSwitch(); + } + } + }, analytics: false, cacheBust: false, scrollTo : !CONFIG.bookmark.enable @@ -28,7 +42,7 @@ document.addEventListener('pjax:success', () => { .bootstrap(); } if (CONFIG.sidebar.display !== 'remove') { - const hasTOC = document.querySelector('.post-toc'); + const hasTOC = document.querySelector('.post-toc:not(.placeholder-toc)'); document.querySelector('.sidebar-inner').classList.toggle('sidebar-nav-active', hasTOC); NexT.utils.activateSidebarPanel(hasTOC ? 0 : 1); NexT.utils.updateSidebarPosition(); diff --git a/source/js/utils.js b/source/js/utils.js index 949cd97f3..ee10f6198 100644 --- a/source/js/utils.js +++ b/source/js/utils.js @@ -122,6 +122,19 @@ NexT.utils = { }); }, + updateActiveNav: function() { + if (!Array.isArray(NexT.utils.sections)) return; + let index = NexT.utils.sections.findIndex(element => { + return element && element.getBoundingClientRect().top > 10; + }); + if (index === -1) { + index = NexT.utils.sections.length - 1; + } else if (index > 0) { + index--; + } + this.activateNavByIndex(index); + }, + registerScrollPercent: function() { const backToTop = document.querySelector('.back-to-top'); const readingProgressBar = document.querySelector('.reading-progress-bar'); @@ -138,16 +151,7 @@ NexT.utils = { readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%'); } } - if (!Array.isArray(NexT.utils.sections)) return; - let index = NexT.utils.sections.findIndex(element => { - return element && element.getBoundingClientRect().top > 10; - }); - if (index === -1) { - index = NexT.utils.sections.length - 1; - } else if (index > 0) { - index--; - } - this.activateNavByIndex(index); + this.updateActiveNav(); }, { passive: true }); backToTop && backToTop.addEventListener('click', () => { @@ -263,7 +267,7 @@ NexT.utils = { }, registerSidebarTOC: function() { - this.sections = [...document.querySelectorAll('.post-toc li a.nav-link')].map(element => { + this.sections = [...document.querySelectorAll('.post-toc:not(.placeholder-toc) li a.nav-link')].map(element => { const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', '')); // TOC item animation navigate. element.addEventListener('click', event => { @@ -281,6 +285,7 @@ NexT.utils = { }); return target; }); + this.updateActiveNav(); }, registerPostReward: function() { @@ -292,18 +297,35 @@ NexT.utils = { }, activateNavByIndex: function(index) { - const target = document.querySelectorAll('.post-toc li a.nav-link')[index]; + const nav = document.querySelector('.post-toc:not(.placeholder-toc) .nav'); + if (!nav) return; + + const navItemList = nav.querySelectorAll('.nav-item'); + const target = navItemList[index]; if (!target || target.classList.contains('active-current')) return; - document.querySelectorAll('.post-toc .active').forEach(element => { - element.classList.remove('active', 'active-current'); + const singleHeight = navItemList[navItemList.length - 1].offsetHeight; + + nav.querySelectorAll('.active').forEach(navItem => { + navItem.classList.remove('active', 'active-current'); }); target.classList.add('active', 'active-current'); - let parent = target.parentNode; - while (!parent.matches('.post-toc')) { - if (parent.matches('li')) parent.classList.add('active'); - parent = parent.parentNode; + + let activateEle = target.querySelector('.nav-child') || target.parentElement; + let navChildHeight = 0; + + while (nav.contains(activateEle)) { + if (activateEle.classList.contains('nav-item')) { + activateEle.classList.add('active'); + } else { // .nav-child or .nav + // scrollHeight isn't reliable for transitioning child items. + // The last nav-item in a list has a margin-bottom of 5px. + navChildHeight += (singleHeight * activateEle.childElementCount) + 5; + activateEle.style.setProperty('--height', `${navChildHeight}px`); + } + activateEle = activateEle.parentElement; } + // Scrolling to center active TOC element if TOC content is taller then viewport. const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar'); if (!document.querySelector('.sidebar-toc-active')) return; @@ -318,7 +340,7 @@ NexT.utils = { updateSidebarPosition: function() { if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; // Expand sidebar on post detail page by default, when post has a toc. - const hasTOC = document.querySelector('.post-toc'); + const hasTOC = document.querySelector('.post-toc:not(.placeholder-toc)'); let display = CONFIG.page.sidebar; if (typeof display !== 'boolean') { // There's no definition sidebar in the page front-matter. @@ -330,31 +352,30 @@ NexT.utils = { }, activateSidebarPanel: function(index) { - const duration = 200; const sidebar = document.querySelector('.sidebar-inner'); - const panel = document.querySelector('.sidebar-panel-container'); - const activeClassName = ['sidebar-toc-active', 'sidebar-overview-active']; + const activeClassNames = ['sidebar-toc-active', 'sidebar-overview-active']; + if (sidebar.classList.contains(activeClassNames[index])) return; - if (sidebar.classList.contains(activeClassName[index])) return; + const panelContainer = sidebar.querySelector('.sidebar-panel-container'); + const tocPanel = panelContainer.firstElementChild; + const overviewPanel = panelContainer.lastElementChild; - window.anime({ - duration, - targets : panel, - easing : 'linear', - opacity : 0, - translateY: [0, -20], - complete : () => { - // Prevent adding TOC to Overview if Overview was selected when close & open sidebar. - sidebar.classList.replace(activeClassName[1 - index], activeClassName[index]); - window.anime({ - duration, - targets : panel, - easing : 'linear', - opacity : [0, 1], - translateY: [-20, 0] - }); + let postTOCHeight = tocPanel.scrollHeight; + // For TOC activation, try to use the animated TOC height + if (index === 0) { + const nav = tocPanel.querySelector('.nav'); + if (nav) { + postTOCHeight = parseInt(nav.style.getPropertyValue('--height'), 10); } - }); + } + const panelHeights = [ + postTOCHeight, + overviewPanel.scrollHeight + ]; + panelContainer.style.setProperty('--inactive-panel-height', `${panelHeights[1 - index]}px`); + panelContainer.style.setProperty('--active-panel-height', `${panelHeights[index]}px`); + + sidebar.classList.replace(activeClassNames[1 - index], activeClassNames[index]); }, getScript: function(src, options = {}, legacyCondition) {