mirror of
https://github.com/next-theme/hexo-theme-next.git
synced 2026-01-17 18:22:33 +00:00
Animate nav item and panel activation (#323)
This commit is contained in:
parent
ec61d98d52
commit
eb743c3b0f
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
.active > .nav-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.active-current > .nav-child {
|
||||
display: block;
|
||||
|
||||
> .nav-item {
|
||||
display: block;
|
||||
.active > .nav-child {
|
||||
height: var(--height, auto);
|
||||
opacity: 1;
|
||||
visibility: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user