Animate nav item and panel activation (#323)

This commit is contained in:
2023-03-01 14:11:20 +08:00 committed by GitHub
parent ec61d98d52
commit eb743c3b0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 184 additions and 71 deletions

View File

@ -1,12 +1,19 @@
// Sidebar Navigation // Sidebar Navigation
.sidebar-nav { .sidebar-nav {
display: none; font-size: $font-size-small;
height: 0;
margin: 0; margin: 0;
padding-bottom: 20px; overflow: hidden;
padding-left: 0; padding-left: 0;
pointer-events: none;
transition: $transition-ease;
transition-property: height, visibility;
visibility: hidden;
.sidebar-nav-active & { .sidebar-nav-active & {
display: block; height: "calc(%sem + 1px)" % $line-height-base;
pointer-events: unset;
visibility: unset;
} }
li { li {
@ -14,7 +21,8 @@
color: $sidebar-nav-color; color: $sidebar-nav-color;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-size: $font-size-small; transition: $transition-ease;
transition-property: border-bottom-color, color;
&.sidebar-nav-overview { &.sidebar-nav-overview {
margin-left: 10px; margin-left: 10px;
@ -29,30 +37,93 @@
.sidebar-toc-active .sidebar-nav-toc, .sidebar-overview-active .sidebar-nav-overview { .sidebar-toc-active .sidebar-nav-toc, .sidebar-overview-active .sidebar-nav-overview {
border-bottom-color: $sidebar-highlight; border-bottom-color: $sidebar-highlight;
color: $sidebar-highlight; color: $sidebar-highlight;
transition-delay: $transition-duration;
&:hover { &:hover {
color: $sidebar-highlight; color: $sidebar-highlight;
} }
} }
// Need for Sidebar/TOC inner scrolling if content taller then viewport. // For TOC/Overview scrolling
.sidebar-panel-container { .sidebar-panel-container {
align-items: start;
display: grid;
flex: 1; flex: 1;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding-top: 0;
transition: padding-top $transition-ease;
.sidebar-nav-active & {
padding-top: 20px;
}
} }
.sidebar-panel { .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 { @keyframes deactivate-sidebar-panel {
// Flexbox layout makes it possible to reorder the child from {
// elements of .site-overview-wrap through the `order` CSS property height: var(--inactive-panel-height, 0);
flex-column(); }
gap: 10px; to {
height: var(--active-panel-height, 0);
}
} }
.sidebar-toc-active .post-toc-wrap { @keyframes activate-sidebar-panel {
display: block; from {
height: var(--inactive-panel-height, auto);
}
to {
height: var(--active-panel-height, auto);
}
} }

View File

@ -5,9 +5,13 @@ if (hexo-config('toc.enable')) {
ol { ol {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0 2px 5px 10px; padding: 0 2px 0 10px;
text-align: left; text-align: left;
> :last-child {
margin-bottom: 5px;
}
> ol { > ol {
padding-left: 0; padding-left: 0;
} }
@ -28,19 +32,21 @@ if (hexo-config('toc.enable')) {
} }
.nav { .nav {
.nav-child { if (not hexo-config('toc.expand_all')) {
display: hexo-config('toc.expand_all') ? block : none; .nav-child {
} --height: 0;
height: 0;
opacity: 0;
overflow: hidden;
transition-property: height, opacity, visibility;
transition: $transition-ease;
visibility: hidden;
}
.active > .nav-child { .active > .nav-child {
display: block; height: var(--height, auto);
} opacity: 1;
visibility: unset;
.active-current > .nav-child {
display: block;
> .nav-item {
display: block;
} }
} }

View File

@ -24,9 +24,10 @@ $orange = #fc6423;
// Transition // Transition
// -------------------------------------------------- // --------------------------------------------------
$transition-ease = .2s ease-in-out; $transition-duration = .2s;
$transition-ease-in = .2s ease-in; $transition-ease = $transition-duration ease-in-out;
$transition-ease-out = .2s ease-out; $transition-ease-in = $transition-duration ease-in;
$transition-ease-out = $transition-duration ease-out;
// Scaffolding // Scaffolding

View File

@ -4,11 +4,25 @@ const pjax = new Pjax({
selectors: [ selectors: [
'head title', 'head title',
'script[type="application/json"]', 'script[type="application/json"]',
'.main-inner', // Precede .main-inner to prevent placeholder TOC changes asap
'.post-toc-wrap', '.post-toc-wrap',
'.main-inner',
'.languages', '.languages',
'.pjax' '.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, analytics: false,
cacheBust: false, cacheBust: false,
scrollTo : !CONFIG.bookmark.enable scrollTo : !CONFIG.bookmark.enable
@ -28,7 +42,7 @@ document.addEventListener('pjax:success', () => {
.bootstrap(); .bootstrap();
} }
if (CONFIG.sidebar.display !== 'remove') { 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); document.querySelector('.sidebar-inner').classList.toggle('sidebar-nav-active', hasTOC);
NexT.utils.activateSidebarPanel(hasTOC ? 0 : 1); NexT.utils.activateSidebarPanel(hasTOC ? 0 : 1);
NexT.utils.updateSidebarPosition(); NexT.utils.updateSidebarPosition();

View File

@ -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() { registerScrollPercent: function() {
const backToTop = document.querySelector('.back-to-top'); const backToTop = document.querySelector('.back-to-top');
const readingProgressBar = document.querySelector('.reading-progress-bar'); const readingProgressBar = document.querySelector('.reading-progress-bar');
@ -138,16 +151,7 @@ NexT.utils = {
readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%'); readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%');
} }
} }
if (!Array.isArray(NexT.utils.sections)) return; this.updateActiveNav();
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);
}, { passive: true }); }, { passive: true });
backToTop && backToTop.addEventListener('click', () => { backToTop && backToTop.addEventListener('click', () => {
@ -263,7 +267,7 @@ NexT.utils = {
}, },
registerSidebarTOC: function() { 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('#', '')); const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', ''));
// TOC item animation navigate. // TOC item animation navigate.
element.addEventListener('click', event => { element.addEventListener('click', event => {
@ -281,6 +285,7 @@ NexT.utils = {
}); });
return target; return target;
}); });
this.updateActiveNav();
}, },
registerPostReward: function() { registerPostReward: function() {
@ -292,18 +297,35 @@ NexT.utils = {
}, },
activateNavByIndex: function(index) { 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; if (!target || target.classList.contains('active-current')) return;
document.querySelectorAll('.post-toc .active').forEach(element => { const singleHeight = navItemList[navItemList.length - 1].offsetHeight;
element.classList.remove('active', 'active-current');
nav.querySelectorAll('.active').forEach(navItem => {
navItem.classList.remove('active', 'active-current');
}); });
target.classList.add('active', 'active-current'); target.classList.add('active', 'active-current');
let parent = target.parentNode;
while (!parent.matches('.post-toc')) { let activateEle = target.querySelector('.nav-child') || target.parentElement;
if (parent.matches('li')) parent.classList.add('active'); let navChildHeight = 0;
parent = parent.parentNode;
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. // 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'); const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar');
if (!document.querySelector('.sidebar-toc-active')) return; if (!document.querySelector('.sidebar-toc-active')) return;
@ -318,7 +340,7 @@ NexT.utils = {
updateSidebarPosition: function() { updateSidebarPosition: function() {
if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
// Expand sidebar on post detail page by default, when post has a toc. // 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; let display = CONFIG.page.sidebar;
if (typeof display !== 'boolean') { if (typeof display !== 'boolean') {
// There's no definition sidebar in the page front-matter. // There's no definition sidebar in the page front-matter.
@ -330,31 +352,30 @@ NexT.utils = {
}, },
activateSidebarPanel: function(index) { activateSidebarPanel: function(index) {
const duration = 200;
const sidebar = document.querySelector('.sidebar-inner'); const sidebar = document.querySelector('.sidebar-inner');
const panel = document.querySelector('.sidebar-panel-container'); const activeClassNames = ['sidebar-toc-active', 'sidebar-overview-active'];
const activeClassName = ['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({ let postTOCHeight = tocPanel.scrollHeight;
duration, // For TOC activation, try to use the animated TOC height
targets : panel, if (index === 0) {
easing : 'linear', const nav = tocPanel.querySelector('.nav');
opacity : 0, if (nav) {
translateY: [0, -20], postTOCHeight = parseInt(nav.style.getPropertyValue('--height'), 10);
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]
});
} }
}); }
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) { getScript: function(src, options = {}, legacyCondition) {