Skip to content

Commit

Permalink
Animate nav item and panel activation (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
PaperStrike authored Mar 1, 2023
1 parent ec61d98 commit eb743c3
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 71 deletions.
97 changes: 84 additions & 13 deletions source/css/_common/outline/sidebar/sidebar-nav.styl
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
// 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 {
border-bottom: 1px solid transparent;
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;
Expand All @@ -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);
}
}
32 changes: 19 additions & 13 deletions source/css/_common/outline/sidebar/sidebar-toc.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
}

Expand Down
7 changes: 4 additions & 3 deletions source/css/_variables/base.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions source/js/pjax.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
101 changes: 61 additions & 40 deletions source/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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 => {
Expand All @@ -281,6 +285,7 @@ NexT.utils = {
});
return target;
});
this.updateActiveNav();
},

registerPostReward: function() {
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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) {
Expand Down

0 comments on commit eb743c3

Please sign in to comment.