mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Adds responsive inline TOC with safe scrollspy
Improves navigation on narrow viewports by moving the TOC into the article and collapsing long lists with a show more/less toggle. Prevents duplicate TOCs and reduces scroll “fighting” by disposing/reinitializing scrollspy on resize and keeping auto-scrolling behavior limited to the right-side desktop panel.
This commit is contained in:
@@ -198,7 +198,78 @@
|
|||||||
border-left-color: #2563eb;
|
border-left-color: #2563eb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* Inline "In this article" TOC (narrow viewports; Learn-like) */
|
||||||
|
.page-toc.page-toc--inline {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 1px solid #eeebee;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #27262b;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__list {
|
||||||
|
list-style: none !important;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__item {
|
||||||
|
list-style: none !important;
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__item::marker {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__item--h3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0.125rem 0 0.125rem 0.75rem;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc--inline .page-toc__link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc__item.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Just-the-Docs renders UL bullets via li::before; disable for inline TOC */
|
||||||
|
.page-toc--inline ul > li::before,
|
||||||
|
.page-toc--inline .page-toc__list > li::before,
|
||||||
|
.page-toc--inline .page-toc__item::before {
|
||||||
|
content: "" !important;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc__toggle {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0.25rem 0 0 0.75rem;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toc__toggle:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<meta name="ffu-right-toc" content="{% if page.right_toc == false %}false{% else %}true{% endif %}">
|
<meta name="ffu-right-toc" content="{% if page.right_toc == false %}false{% else %}true{% endif %}">
|
||||||
|
|
||||||
|
|||||||
+158
-41
@@ -1,6 +1,10 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var scrollSpyDispose = null;
|
||||||
|
var resizeReinitTimerId = null;
|
||||||
|
var inlineMaxVisibleItems = 4;
|
||||||
|
|
||||||
function IsRightTocEnabled() {
|
function IsRightTocEnabled() {
|
||||||
var meta = document.querySelector('meta[name="ffu-right-toc"]');
|
var meta = document.querySelector('meta[name="ffu-right-toc"]');
|
||||||
if (meta && meta.content && meta.content.toLowerCase() === 'false') {
|
if (meta && meta.content && meta.content.toLowerCase() === 'false') {
|
||||||
@@ -18,6 +22,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RemoveExistingToc() {
|
||||||
|
if (scrollSpyDispose) {
|
||||||
|
scrollSpyDispose();
|
||||||
|
scrollSpyDispose = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingTocs = document.querySelectorAll('.page-toc');
|
||||||
|
for (var i = 0; i < existingTocs.length; i++) {
|
||||||
|
existingTocs[i].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrap = document.querySelector('.main-content-wrap');
|
||||||
|
if (wrap) {
|
||||||
|
wrap.classList.remove('has-page-toc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function InsertInlineToc(main, toc) {
|
||||||
|
if (!main || !toc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = main.querySelector('h1');
|
||||||
|
if (title && title.parentNode === main) {
|
||||||
|
if (title.nextSibling) {
|
||||||
|
main.insertBefore(toc, title.nextSibling);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.appendChild(toc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (main.firstChild) {
|
||||||
|
main.insertBefore(toc, main.firstChild);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.appendChild(toc);
|
||||||
|
}
|
||||||
|
|
||||||
function GetHeadings(container) {
|
function GetHeadings(container) {
|
||||||
var headings = container.querySelectorAll('h2, h3');
|
var headings = container.querySelectorAll('h2, h3');
|
||||||
var results = [];
|
var results = [];
|
||||||
@@ -49,9 +94,13 @@
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BuildToc(headings) {
|
function BuildToc(headings, options) {
|
||||||
|
var variant = (options && options.variant) ? options.variant : 'right';
|
||||||
|
var maxVisible = (options && options.maxVisible) ? options.maxVisible : 0;
|
||||||
|
var isInline = 'inline' === variant;
|
||||||
|
|
||||||
var nav = document.createElement('nav');
|
var nav = document.createElement('nav');
|
||||||
nav.className = 'page-toc';
|
nav.className = 'page-toc' + (isInline ? ' page-toc--inline' : '');
|
||||||
nav.setAttribute('aria-label', 'On this page');
|
nav.setAttribute('aria-label', 'On this page');
|
||||||
|
|
||||||
var title = document.createElement('div');
|
var title = document.createElement('div');
|
||||||
@@ -61,6 +110,7 @@
|
|||||||
|
|
||||||
var list = document.createElement('ul');
|
var list = document.createElement('ul');
|
||||||
list.className = 'page-toc__list';
|
list.className = 'page-toc__list';
|
||||||
|
list.id = 'page-toc-list';
|
||||||
|
|
||||||
for (var i = 0; i < headings.length; i++) {
|
for (var i = 0; i < headings.length; i++) {
|
||||||
var item = headings[i];
|
var item = headings[i];
|
||||||
@@ -75,13 +125,61 @@
|
|||||||
|
|
||||||
li.appendChild(a);
|
li.appendChild(a);
|
||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
|
|
||||||
|
if (isInline && maxVisible > 0 && i >= maxVisible) {
|
||||||
|
li.classList.add('is-hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.appendChild(list);
|
nav.appendChild(list);
|
||||||
|
|
||||||
|
if (isInline && maxVisible > 0 && headings.length > maxVisible) {
|
||||||
|
var hiddenCount = headings.length - maxVisible;
|
||||||
|
var isExpanded = false;
|
||||||
|
|
||||||
|
var toggle = document.createElement('button');
|
||||||
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'page-toc__toggle';
|
||||||
|
toggle.setAttribute('aria-controls', list.id);
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
|
function SetToggleText() {
|
||||||
|
if (isExpanded) {
|
||||||
|
toggle.textContent = 'Show less';
|
||||||
|
} else {
|
||||||
|
toggle.textContent = 'Show ' + hiddenCount + ' more';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetHiddenState() {
|
||||||
|
var items = list.querySelectorAll('.page-toc__item');
|
||||||
|
for (var j = 0; j < items.length; j++) {
|
||||||
|
if (j >= maxVisible) {
|
||||||
|
if (isExpanded) {
|
||||||
|
items[j].classList.remove('is-hidden');
|
||||||
|
} else {
|
||||||
|
items[j].classList.add('is-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
|
||||||
|
SetToggleText();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
isExpanded = !isExpanded;
|
||||||
|
SetHiddenState();
|
||||||
|
});
|
||||||
|
|
||||||
|
SetHiddenState();
|
||||||
|
nav.appendChild(toggle);
|
||||||
|
}
|
||||||
|
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SetActiveTocLink(toc, activeId) {
|
function SetActiveTocLink(toc, activeId, keepVisibleInPanel) {
|
||||||
if (!toc) {
|
if (!toc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,12 +193,14 @@
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
link.classList.add('is-active');
|
link.classList.add('is-active');
|
||||||
|
|
||||||
/* Keep the active item visible inside the TOC panel */
|
if (keepVisibleInPanel) {
|
||||||
|
/* Keep the active item visible inside the TOC panel (desktop/right TOC only) */
|
||||||
try {
|
try {
|
||||||
link.scrollIntoView({ block: 'nearest' });
|
link.scrollIntoView({ block: 'nearest' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
link.scrollIntoView();
|
link.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
link.classList.remove('is-active');
|
link.classList.remove('is-active');
|
||||||
}
|
}
|
||||||
@@ -109,12 +209,12 @@
|
|||||||
|
|
||||||
function SetupScrollSpy(main, toc, headings) {
|
function SetupScrollSpy(main, toc, headings) {
|
||||||
if (!main || !toc || !headings || headings.length < 1) {
|
if (!main || !toc || !headings || headings.length < 1) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollspy is desktop-only; on mobile it can cause "fighting" scroll behavior */
|
/* Scrollspy is desktop-only */
|
||||||
if (!IsDesktopViewport()) {
|
if (!IsDesktopViewport()) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var headingElements = [];
|
var headingElements = [];
|
||||||
@@ -126,7 +226,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (headingElements.length < 1) {
|
if (headingElements.length < 1) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeId = null;
|
var activeId = null;
|
||||||
@@ -143,7 +243,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GetCurrentHeadingId() {
|
function GetCurrentHeadingId() {
|
||||||
/* If we're at the bottom, force the last heading active (Learn-like behavior) */
|
/* If we're at the bottom, force the last heading active */
|
||||||
if (IsNearBottomOfPage()) {
|
if (IsNearBottomOfPage()) {
|
||||||
return headingElements[headingElements.length - 1].getAttribute('id');
|
return headingElements[headingElements.length - 1].getAttribute('id');
|
||||||
}
|
}
|
||||||
@@ -177,6 +277,11 @@
|
|||||||
function Update() {
|
function Update() {
|
||||||
ticking = false;
|
ticking = false;
|
||||||
|
|
||||||
|
/* If the viewport becomes narrow after load, avoid scroll fighting */
|
||||||
|
if (!IsDesktopViewport()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Date.now() < lockActiveUntilMs) {
|
if (Date.now() < lockActiveUntilMs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -187,7 +292,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeId = currentId;
|
activeId = currentId;
|
||||||
SetActiveTocLink(toc, activeId);
|
SetActiveTocLink(toc, activeId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OnScrollOrResize() {
|
function OnScrollOrResize() {
|
||||||
@@ -199,11 +304,7 @@
|
|||||||
window.requestAnimationFrame(Update);
|
window.requestAnimationFrame(Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', OnScrollOrResize, { passive: true });
|
function OnTocClick(evt) {
|
||||||
window.addEventListener('resize', OnScrollOrResize);
|
|
||||||
|
|
||||||
/* Update immediately and also when clicking TOC links */
|
|
||||||
toc.addEventListener('click', function (evt) {
|
|
||||||
var target = evt.target;
|
var target = evt.target;
|
||||||
if (!target || !target.classList || !target.classList.contains('page-toc__link')) {
|
if (!target || !target.classList || !target.classList.contains('page-toc__link')) {
|
||||||
return;
|
return;
|
||||||
@@ -223,29 +324,25 @@
|
|||||||
lockActiveUntilMs = Date.now() + 800;
|
lockActiveUntilMs = Date.now() + 800;
|
||||||
|
|
||||||
activeId = id;
|
activeId = id;
|
||||||
SetActiveTocLink(toc, activeId);
|
SetActiveTocLink(toc, activeId, true);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', OnScrollOrResize, { passive: true });
|
||||||
|
window.addEventListener('resize', OnScrollOrResize);
|
||||||
|
toc.addEventListener('click', OnTocClick);
|
||||||
|
|
||||||
Update();
|
Update();
|
||||||
|
|
||||||
|
return function DisposeScrollSpy() {
|
||||||
|
window.removeEventListener('scroll', OnScrollOrResize);
|
||||||
|
window.removeEventListener('resize', OnScrollOrResize);
|
||||||
|
toc.removeEventListener('click', OnTocClick);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function InitRightToc() {
|
function InitRightToc() {
|
||||||
if (!IsRightTocEnabled()) {
|
if (!IsRightTocEnabled()) {
|
||||||
return;
|
RemoveExistingToc();
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop-only TOC: on mobile it interferes with scrolling */
|
|
||||||
if (!IsDesktopViewport()) {
|
|
||||||
var existingWrap = document.querySelector('.main-content-wrap');
|
|
||||||
if (existingWrap) {
|
|
||||||
var existingToc = existingWrap.querySelector('.page-toc');
|
|
||||||
if (existingToc) {
|
|
||||||
existingToc.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
existingWrap.classList.remove('has-page-toc');
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,31 +353,51 @@
|
|||||||
|
|
||||||
var headings = GetHeadings(main);
|
var headings = GetHeadings(main);
|
||||||
if (headings.length < 2) {
|
if (headings.length < 2) {
|
||||||
|
RemoveExistingToc();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsDesktopViewport()) {
|
||||||
|
RemoveExistingToc();
|
||||||
|
|
||||||
var wrap = document.querySelector('.main-content-wrap');
|
var wrap = document.querySelector('.main-content-wrap');
|
||||||
var content = document.querySelector('.main-content');
|
if (!wrap) {
|
||||||
if (!wrap || !content) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wrap.querySelector('.page-toc')) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wrap.classList.add('has-page-toc');
|
wrap.classList.add('has-page-toc');
|
||||||
|
|
||||||
var toc = BuildToc(headings);
|
var toc = BuildToc(headings, { variant: 'right' });
|
||||||
wrap.appendChild(toc);
|
wrap.appendChild(toc);
|
||||||
|
|
||||||
SetupScrollSpy(main, toc, headings);
|
scrollSpyDispose = SetupScrollSpy(main, toc, headings);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Narrow viewports: place TOC at the top of the article (Learn-like) */
|
||||||
|
RemoveExistingToc();
|
||||||
|
|
||||||
|
var inlineToc = BuildToc(headings, { variant: 'inline', maxVisible: inlineMaxVisibleItems });
|
||||||
|
InsertInlineToc(main, inlineToc);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnViewportResize() {
|
||||||
|
if (null !== resizeReinitTimerId) {
|
||||||
|
window.clearTimeout(resizeReinitTimerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeReinitTimerId = window.setTimeout(InitRightToc, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', InitRightToc);
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
InitRightToc();
|
||||||
|
window.addEventListener('resize', OnViewportResize);
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InitRightToc();
|
InitRightToc();
|
||||||
|
window.addEventListener('resize', OnViewportResize);
|
||||||
})();
|
})();
|
||||||
Reference in New Issue
Block a user