mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
133e70ea89
Prevents mobile scroll “fighting” by limiting scrollspy and the page TOC to desktop-sized viewports. Removes any existing TOC markup and related layout class when below the desktop breakpoint to avoid interfering with touch scrolling.
286 lines
7.8 KiB
JavaScript
286 lines
7.8 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
function IsRightTocEnabled() {
|
|
var meta = document.querySelector('meta[name="ffu-right-toc"]');
|
|
if (meta && meta.content && meta.content.toLowerCase() === 'false') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function IsDesktopViewport() {
|
|
try {
|
|
return window.matchMedia && window.matchMedia('(min-width: 66.5rem)').matches;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function GetHeadings(container) {
|
|
var headings = container.querySelectorAll('h2, h3');
|
|
var results = [];
|
|
|
|
for (var i = 0; i < headings.length; i++) {
|
|
var heading = headings[i];
|
|
|
|
if (heading.classList.contains('no_toc')) {
|
|
continue;
|
|
}
|
|
|
|
var id = heading.getAttribute('id');
|
|
if (!id) {
|
|
continue;
|
|
}
|
|
|
|
var text = (heading.textContent || '').trim();
|
|
if (!text) {
|
|
continue;
|
|
}
|
|
|
|
results.push({
|
|
level: heading.tagName.toLowerCase(),
|
|
id: id,
|
|
text: text
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function BuildToc(headings) {
|
|
var nav = document.createElement('nav');
|
|
nav.className = 'page-toc';
|
|
nav.setAttribute('aria-label', 'On this page');
|
|
|
|
var title = document.createElement('div');
|
|
title.className = 'page-toc__title';
|
|
title.textContent = 'In this article';
|
|
nav.appendChild(title);
|
|
|
|
var list = document.createElement('ul');
|
|
list.className = 'page-toc__list';
|
|
|
|
for (var i = 0; i < headings.length; i++) {
|
|
var item = headings[i];
|
|
|
|
var li = document.createElement('li');
|
|
li.className = 'page-toc__item page-toc__item--' + item.level;
|
|
|
|
var a = document.createElement('a');
|
|
a.className = 'page-toc__link';
|
|
a.href = '#' + item.id;
|
|
a.textContent = item.text;
|
|
|
|
li.appendChild(a);
|
|
list.appendChild(li);
|
|
}
|
|
|
|
nav.appendChild(list);
|
|
return nav;
|
|
}
|
|
|
|
function SetActiveTocLink(toc, activeId) {
|
|
if (!toc) {
|
|
return;
|
|
}
|
|
|
|
var links = toc.querySelectorAll('.page-toc__link');
|
|
for (var i = 0; i < links.length; i++) {
|
|
var link = links[i];
|
|
var href = link.getAttribute('href') || '';
|
|
var isActive = ('#' + activeId) === href;
|
|
|
|
if (isActive) {
|
|
link.classList.add('is-active');
|
|
|
|
/* Keep the active item visible inside the TOC panel */
|
|
try {
|
|
link.scrollIntoView({ block: 'nearest' });
|
|
} catch (e) {
|
|
link.scrollIntoView();
|
|
}
|
|
} else {
|
|
link.classList.remove('is-active');
|
|
}
|
|
}
|
|
}
|
|
|
|
function SetupScrollSpy(main, toc, headings) {
|
|
if (!main || !toc || !headings || headings.length < 1) {
|
|
return;
|
|
}
|
|
|
|
/* Scrollspy is desktop-only; on mobile it can cause "fighting" scroll behavior */
|
|
if (!IsDesktopViewport()) {
|
|
return;
|
|
}
|
|
|
|
var headingElements = [];
|
|
for (var i = 0; i < headings.length; i++) {
|
|
var el = document.getElementById(headings[i].id);
|
|
if (el) {
|
|
headingElements.push(el);
|
|
}
|
|
}
|
|
|
|
if (headingElements.length < 1) {
|
|
return;
|
|
}
|
|
|
|
var activeId = null;
|
|
var ticking = false;
|
|
var lockActiveUntilMs = 0;
|
|
|
|
function IsNearBottomOfPage() {
|
|
var thresholdPx = 24;
|
|
var scrollY = window.scrollY || window.pageYOffset || 0;
|
|
var viewportBottom = scrollY + window.innerHeight;
|
|
var pageHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
|
|
|
|
return viewportBottom >= (pageHeight - thresholdPx);
|
|
}
|
|
|
|
function GetCurrentHeadingId() {
|
|
/* If we're at the bottom, force the last heading active (Learn-like behavior) */
|
|
if (IsNearBottomOfPage()) {
|
|
return headingElements[headingElements.length - 1].getAttribute('id');
|
|
}
|
|
|
|
/* Choose the heading closest to the top "activation line" */
|
|
var activationLine = 16;
|
|
var current = null;
|
|
|
|
for (var i = 0; i < headingElements.length; i++) {
|
|
var rectTop = headingElements[i].getBoundingClientRect().top;
|
|
|
|
if (rectTop <= activationLine) {
|
|
current = headingElements[i];
|
|
continue;
|
|
}
|
|
|
|
if (null === current) {
|
|
current = headingElements[i];
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (null === current) {
|
|
current = headingElements[0];
|
|
}
|
|
|
|
return current.getAttribute('id');
|
|
}
|
|
|
|
function Update() {
|
|
ticking = false;
|
|
|
|
if (Date.now() < lockActiveUntilMs) {
|
|
return;
|
|
}
|
|
|
|
var currentId = GetCurrentHeadingId();
|
|
if (!currentId || currentId === activeId) {
|
|
return;
|
|
}
|
|
|
|
activeId = currentId;
|
|
SetActiveTocLink(toc, activeId);
|
|
}
|
|
|
|
function OnScrollOrResize() {
|
|
if (ticking) {
|
|
return;
|
|
}
|
|
|
|
ticking = true;
|
|
window.requestAnimationFrame(Update);
|
|
}
|
|
|
|
window.addEventListener('scroll', OnScrollOrResize, { passive: true });
|
|
window.addEventListener('resize', OnScrollOrResize);
|
|
|
|
/* Update immediately and also when clicking TOC links */
|
|
toc.addEventListener('click', function (evt) {
|
|
var target = evt.target;
|
|
if (!target || !target.classList || !target.classList.contains('page-toc__link')) {
|
|
return;
|
|
}
|
|
|
|
var href = target.getAttribute('href') || '';
|
|
if (href.charAt(0) !== '#') {
|
|
return;
|
|
}
|
|
|
|
var id = href.substring(1);
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
/* Prevent scrollspy from immediately overriding the clicked section */
|
|
lockActiveUntilMs = Date.now() + 800;
|
|
|
|
activeId = id;
|
|
SetActiveTocLink(toc, activeId);
|
|
});
|
|
|
|
Update();
|
|
}
|
|
|
|
function InitRightToc() {
|
|
if (!IsRightTocEnabled()) {
|
|
return;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
var main = document.querySelector('.main-content main');
|
|
if (!main) {
|
|
return;
|
|
}
|
|
|
|
var headings = GetHeadings(main);
|
|
if (headings.length < 2) {
|
|
return;
|
|
}
|
|
|
|
var wrap = document.querySelector('.main-content-wrap');
|
|
var content = document.querySelector('.main-content');
|
|
if (!wrap || !content) {
|
|
return;
|
|
}
|
|
|
|
if (wrap.querySelector('.page-toc')) {
|
|
return;
|
|
}
|
|
|
|
wrap.classList.add('has-page-toc');
|
|
|
|
var toc = BuildToc(headings);
|
|
wrap.appendChild(toc);
|
|
|
|
SetupScrollSpy(main, toc, headings);
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', InitRightToc);
|
|
return;
|
|
}
|
|
|
|
InitRightToc();
|
|
})(); |