mirror of
https://github.com/next-theme/hexo-theme-next.git
synced 2026-01-19 18:42:34 +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 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user