// ==UserScript==
// @name Roblox FACS Numbering
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Add numbers next to FACS names for easier reference | Discord: https://discord.gg/BE7k9Xxm5z
// @author Cloud Guy
// @license MIT
// @match https://create.roblox.com/docs/art/characters/facial-animation/facs-poses-reference*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const FACS = ['EyesLookDown','EyesLookLeft','EyesLookRight','EyesLookUp','JawDrop','LeftEyeClosed','LeftLipCornerPuller','LeftLipStretcher','LeftLowerLipDepressor','LeftUpperLipRaiser','LipsTogether','Pucker','RightEyeClosed','RightLipCornerPuller','RightLipStretcher','RightLowerLipDepressor','RightUpperLipRaiser','ChinRaiser','ChinRaiserUpperLip','FlatPucker','Funneler','LowerLipSuck','LipPresser','MouthLeft','MouthRight','UpperLipSuck','LeftCheekPuff','LeftDimpler','LeftLipCornerDown','RightCheekPuff','RightDimpler','RightLipCornerDown','JawLeft','JawRight','Corrugator','LeftBrowLowerer','LeftOuterBrowRaiser','LeftNoseWrinkler','LeftInnerBrowRaiser','RightBrowLowerer','RightOuterBrowRaiser','RightNoseWrinkler','RightInnerBrowRaiser','LeftCheekRaiser','LeftEyeUpperLidRaiser','RightCheekRaiser','RightEyeUpperLidRaiser','TongueDown','TongueOut','TongueUp'];
const PAIRS = {
'LeftEyeClosed': 'RightEyeClosed',
'RightEyeClosed': 'LeftEyeClosed',
'LeftLipCornerPuller': 'RightLipCornerPuller',
'RightLipCornerPuller': 'LeftLipCornerPuller',
'LeftLipStretcher': 'RightLipStretcher',
'RightLipStretcher': 'LeftLipStretcher',
'LeftLowerLipDepressor': 'RightLowerLipDepressor',
'RightLowerLipDepressor': 'LeftLowerLipDepressor',
'LeftUpperLipRaiser': 'RightUpperLipRaiser',
'RightUpperLipRaiser': 'LeftUpperLipRaiser',
'LeftCheekPuff': 'RightCheekPuff',
'RightCheekPuff': 'LeftCheekPuff',
'LeftDimpler': 'RightDimpler',
'RightDimpler': 'LeftDimpler',
'LeftLipCornerDown': 'RightLipCornerDown',
'RightLipCornerDown': 'LeftLipCornerDown',
'LeftBrowLowerer': 'RightBrowLowerer',
'RightBrowLowerer': 'LeftBrowLowerer',
'LeftOuterBrowRaiser': 'RightOuterBrowRaiser',
'RightOuterBrowRaiser': 'LeftOuterBrowRaiser',
'LeftNoseWrinkler': 'RightNoseWrinkler',
'RightNoseWrinkler': 'LeftNoseWrinkler',
'LeftInnerBrowRaiser': 'RightInnerBrowRaiser',
'RightInnerBrowRaiser': 'LeftInnerBrowRaiser',
'LeftCheekRaiser': 'RightCheekRaiser',
'RightCheekRaiser': 'LeftCheekRaiser',
'LeftEyeUpperLidRaiser': 'RightEyeUpperLidRaiser',
'RightEyeUpperLidRaiser': 'LeftEyeUpperLidRaiser',
'JawLeft': 'JawRight',
'JawRight': 'JawLeft',
'MouthLeft': 'MouthRight',
'MouthRight': 'MouthLeft',
'EyesLookLeft': 'EyesLookRight',
'EyesLookRight': 'EyesLookLeft',
'EyesLookUp': 'EyesLookDown',
'EyesLookDown': 'EyesLookUp'
};
const THEME = {
primary: '#60A5FA',
primaryDark: '#3B82F6',
accent: '#93C5FD',
background: '#111216',
cardBg: '#1a1b20',
textPrimary: '#ffffff',
textSecondary: '#cccccc',
highlight: 'rgba(96, 165, 250, 0.10)',
highlightHover: 'rgba(96, 165, 250, 0.18)',
inactive: '#3a3b40',
inactiveDark: '#2a2b30',
shadow: 'rgba(0, 0, 0, 0.3)'
};
const elemCache = {
sections: [],
tocItems: [],
lastHighlighted: '',
sidebar: null,
discordButton: null,
toggleButton: null,
indicator: null,
marker: null
};
function addStyles() {
const css = `
.facs-highlight {
background: linear-gradient(135deg, ${THEME.highlight} 0%, rgba(96, 165, 250, 0.18) 100%);
font-weight: 600;
border-radius: 8px;
box-shadow: 0 4px 12px ${THEME.shadow}, inset 0 1px 0 rgba(255, 255, 255, 0.05);
opacity: 1;
transform: translateX(0);
transition: all 0.2s ease-out;
}
.facs-highlight-sidebar {
border-left: 3px solid ${THEME.primary};
padding-left: 8px;
}
.facs-highlight:hover {
background: linear-gradient(135deg, ${THEME.highlightHover} 0%, rgba(96, 165, 250, 0.28) 100%);
transform: translateX(4px) scale(1.02);
box-shadow: 0 6px 20px ${THEME.shadow}, inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: all 0.15s ease-out;
}
.toc-active {
background: linear-gradient(135deg, rgba(0, 162, 255, 0.15) 0%, rgba(0, 102, 204, 0.25) 100%);
font-weight: 600;
border-left: 3px solid ${THEME.primary};
padding-left: 8px;
border-radius: 8px;
box-shadow: 0 2px 8px ${THEME.shadow}, inset 0 1px 0 rgba(255, 255, 255, 0.05);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.toc-active:hover {
background: linear-gradient(135deg, rgba(0, 162, 255, 0.25) 0%, rgba(0, 102, 204, 0.35) 100%);
transform: translateX(4px);
}
#facs-discord {
position: fixed;
top: 15px;
right: 20px;
background: linear-gradient(135deg, ${THEME.inactive} 0%, ${THEME.inactiveDark} 100%);
color: ${THEME.textSecondary};
padding: 10px;
border-radius: 12px;
text-decoration: none;
z-index: 9999;
box-shadow: 0 4px 16px ${THEME.shadow};
display: flex;
align-items: center;
justify-content: center;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid ${THEME.cardBg};
width: 40px;
height: 40px;
}
#facs-discord:hover {
transform: scale(1.15) translateY(-4px);
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4), 0 4px 15px rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, #5865F2 0%, #4752C4 100%);
color: ${THEME.textPrimary};
border-color: #7289DA;
animation: pulse 1.5s infinite;
}
#facs-highlight-toggle {
position: fixed;
top: 70px;
right: 20px;
background: linear-gradient(135deg, ${THEME.inactive} 0%, ${THEME.inactiveDark} 100%);
color: ${THEME.textPrimary};
padding: 8px 12px;
border-radius: 10px;
border: none;
font-weight: 500;
font-size: 12px;
font-family: system-ui, -apple-system, sans-serif;
cursor: pointer;
z-index: 9999;
box-shadow: 0 3px 12px ${THEME.shadow};
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: auto;
letter-spacing: 0.025em;
}
#facs-highlight-toggle:hover {
transform: scale(1.08) translateY(-2px);
box-shadow: 0 8px 25px rgba(96, 165, 250, 0.3), 0 4px 12px ${THEME.shadow};
animation: togglePulse 1.5s infinite;
}
#facs-highlight-toggle.enabled {
background: linear-gradient(135deg, ${THEME.primary} 0%, ${THEME.primaryDark} 100%);
}
#facs-highlight-toggle.enabled:hover {
background: linear-gradient(135deg, ${THEME.accent} 0%, ${THEME.primary} 100%);
}
#facs-scrollbar-indicator {
position: fixed;
top: 15%;
width: 3px;
height: 70%;
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
z-index: 9998;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
#facs-indicator-marker {
position: absolute;
left: -2px;
width: 7px;
height: 18px;
background: ${THEME.primary};
border-radius: 3px;
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
transition: all 0.15s ease;
opacity: 0;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4), 0 4px 15px rgba(255, 255, 255, 0.1), 0 0 0 0 rgba(88, 101, 242, 0.7); }
50% { box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4), 0 4px 15px rgba(255, 255, 255, 0.1), 0 0 0 8px rgba(88, 101, 242, 0); }
}
@keyframes togglePulse {
0%, 100% { box-shadow: 0 8px 25px rgba(96, 165, 250, 0.3), 0 4px 12px ${THEME.shadow}, 0 0 0 0 rgba(96, 165, 250, 0.7); }
50% { box-shadow: 0 8px 25px rgba(96, 165, 250, 0.3), 0 4px 12px ${THEME.shadow}, 0 0 0 8px rgba(96, 165, 250, 0); }
}
`;
const styleElement = document.createElement('style');
styleElement.textContent = css;
document.head.appendChild(styleElement);
}
const createTextNodeWalker = (root) => {
return document.createTreeWalker(
root || document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
};
const throttle = (callback, delay) => {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return callback(...args);
};
};
function numberFACS() {
try {
const walker = createTextNodeWalker();
let node;
function processTextNodes(deadline) {
const startTime = performance.now();
while ((node = walker.nextNode()) && (performance.now() - startTime < 10)) {
const text = node.textContent.trim();
const index = FACS.indexOf(text);
if (index !== -1 && !text.match(/^\d+\.\s/)) {
node.textContent = `${index + 1}. ${text}`;
}
}
if (node) {
requestIdleCallback(processTextNodes);
} else {
numberSidebarElements();
}
}
requestIdleCallback(processTextNodes);
} catch (e) {
console.log('FACS numbering error:', e);
}
}
function numberSidebarElements() {
try {
const sidebarLinksSelector = 'nav a, .toc a, aside a, [class*="sidebar"] a, [class*="navigation"] a, [class*="nav"] a';
const sidebarLinks = document.querySelectorAll(sidebarLinksSelector);
const fragment = document.createDocumentFragment();
sidebarLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && href.startsWith('#')) {
const facsName = href.substring(1);
const index = FACS.indexOf(facsName);
if (index !== -1) {
const text = link.textContent.trim();
if (!text.match(/^\d+\.\s/)) {
const updatedLink = link.cloneNode(true);
updatedLink.textContent = `${index + 1}. ${text}`;
fragment.appendChild(updatedLink);
link.parentNode.replaceChild(updatedLink, link);
}
}
}
});
const sidebarContainers = document.querySelectorAll('nav, .toc, aside, [class*="sidebar"], [class*="navigation"], [class*="nav"]');
sidebarContainers.forEach(container => {
const walker = createTextNodeWalker(container);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
const index = FACS.indexOf(text);
if (index !== -1 && !node.textContent.match(/^\d+\.\s/)) {
node.textContent = `${index + 1}. ${text}`;
}
}
});
} catch (e) {
console.log('Sidebar numbering error:', e);
}
}
function forceNumberAllFACS() {
try {
const walker = createTextNodeWalker();
let node;
const facsIndexMap = new Map();
FACS.forEach((name, index) => {
facsIndexMap.set(name, index);
});
while (node = walker.nextNode()) {
const text = node.textContent.trim();
const index = facsIndexMap.get(text);
if (index !== undefined) {
node.textContent = `${index + 1}. ${text}`;
continue;
}
for (let i = 0; i < FACS.length; i++) {
if (text === FACS[i] || text.endsWith(FACS[i])) {
if (!text.startsWith(`${i + 1}.`)) {
node.textContent = `${i + 1}. ${FACS[i]}`;
}
break;
}
}
}
numberSidebarElements();
} catch (e) {
console.log('Force numbering error:', e);
}
}
function addDiscord() {
try {
if (document.getElementById('facs-discord') || elemCache.discordButton) return;
const link = document.createElement('a');
link.id = 'facs-discord';
link.href = 'https://discord.gg/BE7k9Xxm5z';
link.target = '_blank';
link.rel = 'noopener';
link.title = 'Join My Discord';
link.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink: 0;">
<path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.3-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12z"/>
</svg>`;
link.addEventListener('click', function() {
this.style.transform = 'scale(0.95)';
setTimeout(() => {
this.style.transform = 'scale(1.1) translateY(-2px)';
}, 100);
});
elemCache.discordButton = link;
document.body.appendChild(link);
} catch (e) {
console.log('Discord button error:', e);
}
}
function addHighlightToggle() {
try {
if (document.getElementById('facs-highlight-toggle') || elemCache.toggleButton) return;
const toggleButton = document.createElement('button');
toggleButton.id = 'facs-highlight-toggle';
toggleButton.title = 'Toggle FACS Highlighting';
document.body.setAttribute('data-facs-highlight', 'disabled');
toggleButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
<path d="M3 3L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Toggle Highlight
`;
toggleButton.addEventListener('click', toggleHighlightMode);
elemCache.toggleButton = toggleButton;
document.body.appendChild(toggleButton);
} catch (e) {
console.log('Highlight toggle button error:', e);
}
}
function toggleHighlightMode() {
const button = elemCache.toggleButton || document.getElementById('facs-highlight-toggle');
if (!button) return;
button.style.transform = 'scale(1.05) translateY(-2px)';
button.style.transition = 'transform 0.1s ease';
setTimeout(() => {
button.style.transform = 'scale(1) translateY(0)';
button.style.transition = 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)';
}, 100);
const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
if (isHighlightEnabled) {
document.body.setAttribute('data-facs-highlight', 'disabled');
button.classList.remove('enabled');
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
<path d="M3 3L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Toggle Highlight
`;
document.querySelectorAll('.facs-highlight').forEach(el => {
el.style.transition = 'all 0.15s ease-out';
el.style.background = 'transparent';
el.style.boxShadow = 'none';
requestAnimationFrame(() => {
el.classList.remove('facs-highlight', 'facs-highlight-sidebar');
setTimeout(() => {
el.style.cssText = '';
}, 150);
});
});
document.querySelectorAll('.toc-active').forEach(el => {
el.style.transition = 'all 0.25s ease';
el.style.transform = 'scale(0.95)';
el.style.opacity = '0.7';
requestAnimationFrame(() => {
setTimeout(() => {
el.classList.remove('toc-active');
el.style.transform = '';
el.style.opacity = '';
el.style.transition = '';
}, 250);
});
});
if (elemCache.indicator) {
elemCache.indicator.style.opacity = '0';
}
} else {
document.body.setAttribute('data-facs-highlight', 'enabled');
button.classList.add('enabled');
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
</svg>
Toggle Highlight
`;
highlightPairs();
checkActiveSection();
}
}
function highlightPairs() {
try {
const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
if (!isHighlightEnabled) return;
document.querySelectorAll('.facs-highlight').forEach(el => {
el.classList.remove('facs-highlight', 'facs-highlight-sidebar');
el.style.cssText = '';
});
const hash = window.location.hash.slice(1);
if (!hash) return;
const matchedFACS = FACS.find(f => f.toLowerCase() === hash.toLowerCase());
if (!matchedFACS) return;
const pair = PAIRS[matchedFACS];
if (!pair && matchedFACS !== 'neutral') return;
const fragment = document.createDocumentFragment();
const walker = createTextNodeWalker();
let node;
const targetTexts = new Set([matchedFACS]);
if (pair) targetTexts.add(pair);
function processNodes(deadline) {
const startTime = performance.now();
while ((node = walker.nextNode()) && (performance.now() - startTime < 5)) {
const text = node.textContent.replace(/^\d+\.\s*/, '').trim();
if (targetTexts.has(text)) {
const parent = node.parentElement;
if (parent) {
parent.classList.add('facs-highlight');
parent.style.opacity = '0';
parent.style.transform = 'translateX(-8px)';
requestAnimationFrame(() => {
parent.style.opacity = '1';
parent.style.transform = 'translateX(0)';
if (parent.tagName === 'A' || parent.closest('nav, .toc')) {
parent.classList.add('facs-highlight-sidebar');
}
});
}
}
}
if (node) {
requestAnimationFrame(() => processNodes());
} else {
elemCache.lastHighlighted = hash;
updateScrollbarIndicator();
}
}
requestAnimationFrame(processNodes);
} catch (e) {
console.log('Highlight error:', e);
}
}
function addScrollbarIndicator() {
try {
if (document.getElementById('facs-scrollbar-indicator') || elemCache.indicator) return;
let sidebar = document.querySelector('nav, .sidebar, [class*="sidebar"], [class*="navigation"]');
if (!sidebar) {
const facsElements = document.querySelectorAll('a[href^="#EyesLook"], a[href^="#JawDrop"]');
if (facsElements.length > 0) {
sidebar = facsElements[0].closest('nav, div, ul, ol') || facsElements[0].parentElement;
}
}
if (!sidebar) return;
elemCache.sidebar = sidebar;
const indicator = document.createElement('div');
indicator.id = 'facs-scrollbar-indicator';
const marker = document.createElement('div');
marker.id = 'facs-indicator-marker';
indicator.appendChild(marker);
const updateIndicatorPosition = () => {
const sidebarRect = elemCache.sidebar.getBoundingClientRect();
indicator.style.left = `${sidebarRect.right + 8}px`;
};
elemCache.indicator = indicator;
elemCache.marker = marker;
updateIndicatorPosition();
window.addEventListener('resize', throttle(updateIndicatorPosition, 100));
document.body.appendChild(indicator);
} catch (e) {
console.log('Scrollbar indicator error:', e);
}
}
function updateScrollbarIndicator() {
try {
const indicator = elemCache.indicator || document.getElementById('facs-scrollbar-indicator');
const marker = elemCache.marker || document.getElementById('facs-indicator-marker');
if (!indicator || !marker) return;
const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
if (!isHighlightEnabled) {
indicator.style.opacity = '0';
return;
}
const activeHighlight = document.querySelector('.facs-highlight');
if (activeHighlight) {
indicator.style.opacity = '1';
const facsMap = new Map();
FACS.forEach(name => facsMap.set(name, true));
const allFacsItems = document.querySelectorAll('a[href^="#"], nav a, .sidebar a');
let facsIndex = -1;
let totalFacs = 0;
const currentHash = window.location.hash.slice(1);
allFacsItems.forEach((item) => {
const href = item.getAttribute('href');
if (href && href.startsWith('#')) {
const facsName = href.substring(1);
if (facsMap.has(facsName)) {
if (item.classList.contains('facs-highlight') || item.textContent.includes(currentHash)) {
facsIndex = totalFacs;
}
totalFacs++;
}
}
});
if (facsIndex >= 0 && totalFacs > 0) {
const relativePosition = facsIndex / Math.max(1, totalFacs - 1);
const indicatorHeight = indicator.offsetHeight;
const markerPosition = relativePosition * (indicatorHeight - 18);
marker.style.top = `${Math.max(0, Math.min(markerPosition, indicatorHeight - 18))}px`;
marker.style.opacity = '1';
} else {
marker.style.opacity = '0';
}
} else {
marker.style.opacity = '0';
indicator.style.opacity = '0';
}
} catch (e) {
console.log('Scrollbar indicator update error:', e);
}
}
function collectElements() {
const facsSet = new Set(FACS);
elemCache.sections = Array.from(document.querySelectorAll('h2, h3, h4')).filter(heading => {
const headingText = heading.textContent;
for (const facs of FACS) {
if (headingText.includes(facs)) {
return true;
}
}
return heading.id && facsSet.has(heading.id);
});
elemCache.tocItems = Array.from(document.querySelectorAll('nav a[href^="#"], .toc a[href^="#"]'));
}
function updateTOCActiveState(sectionId) {
const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
if (!isHighlightEnabled) return;
const tocItems = elemCache.tocItems.length ? elemCache.tocItems :
document.querySelectorAll('nav a[href^="#"], .toc a[href^="#"]');
tocItems.forEach(item => {
item.classList.remove('toc-active');
});
const activeItem = tocItems.find(item => item.getAttribute('href') === `#${sectionId}`);
if (activeItem) {
activeItem.classList.add('toc-active');
}
}
function checkActiveSection() {
try {
const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
if (!isHighlightEnabled) return;
if (elemCache.sections.length === 0 || elemCache.tocItems.length === 0) {
collectElements();
}
const scrollPosition = window.scrollY || document.documentElement.scrollTop;
const viewportHeight = window.innerHeight;
const referencePoint = scrollPosition + (viewportHeight * 0.2);
let activeSection = null;
let minDistance = Infinity;
elemCache.sections.forEach(section => {
const rect = section.getBoundingClientRect();
const sectionTop = rect.top + scrollPosition;
const distance = Math.abs(sectionTop - referencePoint);
if (distance < minDistance) {
minDistance = distance;
activeSection = section;
}
});
if (activeSection) {
let sectionId = activeSection.id;
if (!sectionId) {
for (const facs of FACS) {
if (activeSection.textContent.includes(facs)) {
sectionId = facs;
break;
}
}
}
if (sectionId && sectionId !== window.location.hash.slice(1)) {
history.replaceState(null, null, `#${sectionId}`);
highlightPairs();
updateTOCActiveState(sectionId);
}
}
} catch (e) {
console.log('Section check error:', e);
}
}
function init() {
addStyles();
numberFACS();
requestIdleCallback(() => {
forceNumberAllFACS();
addDiscord();
addHighlightToggle();
addScrollbarIndicator();
collectElements();
setTimeout(() => {
checkActiveSection();
}, 500);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.addEventListener('hashchange', () => {
highlightPairs();
const hash = window.location.hash.slice(1);
if (hash) {
updateTOCActiveState(hash);
}
});
window.addEventListener('scroll', throttle(checkActiveSection, 150), { passive: true });
const observer = new MutationObserver(function(mutations) {
const needsUpdate = mutations.some(mutation =>
mutation.type === 'childList' ||
(mutation.type === 'attributes' && mutation.target.nodeName !== 'BODY')
);
if (needsUpdate) {
clearTimeout(window.facsUpdateTimeout);
window.facsUpdateTimeout = setTimeout(() => {
forceNumberAllFACS();
collectElements();
checkActiveSection();
}, 300);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'id', 'href']
});
})();