Igor150195

tocmenu

Dec 8th, 2025 (edited)
41
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <script>
  2. (function () {
  3.  
  4. // === Глобальный счётчик уникальных H2 для всех wrap ===
  5. let globalHeadingIndex = 0;
  6.  
  7. /* ===========================================================
  8.     Стабильная работа с URL только на уровне article-wrapper
  9.    =========================================================== */
  10.  
  11. let currentWrapUrl = null;
  12. let lastSetUrl = null;
  13. let historyUpdateTimeout = null;
  14.  
  15. // Мягкое обновление URL
  16. function replaceUrlThrottled(url) {
  17.     if (lastSetUrl === url) return;
  18.  
  19.     clearTimeout(historyUpdateTimeout);
  20.     historyUpdateTimeout = setTimeout(() => {
  21.         history.replaceState(null, '', url);
  22.         lastSetUrl = url;
  23.     }, 200);
  24. }
  25.  
  26. // Получаем активный article-wrapper при каждом скролле
  27. function getActiveWrap() {
  28.     const pos = window.scrollY + window.innerHeight * 0.3;
  29.  
  30.     let active = null;
  31.  
  32.     document.querySelectorAll(".article-wrapper").forEach(wrap => {
  33.         const rect = wrap.getBoundingClientRect();
  34.         const top = window.scrollY + rect.top;
  35.         const bottom = top + rect.height;
  36.  
  37.         if (pos >= top && pos <= bottom) {
  38.             active = wrap;
  39.         }
  40.     });
  41.  
  42.     return active;
  43. }
  44.  
  45. function updateActiveWrap() {
  46.     const activeWrap = getActiveWrap();
  47.     if (!activeWrap) return;
  48.  
  49.     const url = activeWrap.querySelector(".dynamic-lazy-url span")
  50.         ?.textContent.trim();
  51.  
  52.     if (!url) return;
  53.  
  54.     if (url !== currentWrapUrl) {
  55.         replaceUrlThrottled(url);
  56.         currentWrapUrl = url;
  57.     }
  58. }
  59.  
  60. /* ==========================================
  61.    Генерация TOC для конкретного article-wrapper
  62.    ========================================== */
  63. function initTOCForWrap(wrap) {
  64.     if (!wrap || wrap.dataset.tocReady) return;
  65.  
  66.     const tocContainer = wrap.querySelector(".toc-table");
  67.     const articleContent = wrap.querySelector(".article-content");
  68.  
  69.     if (!tocContainer || !articleContent) return;
  70.  
  71.     function slugify(str) {
  72.         return String(str).toLowerCase().trim()
  73.             .replace(/[^\w\s-]/g, '')
  74.             .replace(/\s+/g, '-')
  75.             .replace(/-+/g, '-');
  76.     }
  77.  
  78.     const headings = articleContent.querySelectorAll("h2");
  79.     headings.forEach(heading => {
  80.         const title = heading.textContent.trim();
  81.         const anchorId = `toc-${slugify(title)}-${globalHeadingIndex}`;
  82.  
  83.         heading.setAttribute("id", anchorId);
  84.  
  85.         const a = document.createElement("a");
  86.         a.href = "javascript:void(0)";
  87.         a.dataset.anchor = anchorId;
  88.         a.textContent = title;
  89.         a.classList.add("articles-link");
  90.  
  91.         a.addEventListener("click", (e) => {
  92.             e.preventDefault();
  93.             const id = a.dataset.anchor;
  94.             const target = document.getElementById(id);
  95.  
  96.             if (target) {
  97.                 const y = target.getBoundingClientRect().top + window.scrollY - 30;
  98.                 window.scrollTo({ top: y, behavior: "smooth" });
  99.             }
  100.  
  101.             // При клике меняется URL на URL текущего wrap
  102.             const dynamicUrlSpan = wrap.querySelector(".dynamic-lazy-url span");
  103.             if (dynamicUrlSpan) {
  104.                 const articleUrl = dynamicUrlSpan.textContent.trim();
  105.                 history.pushState(null, '', articleUrl);
  106.                 lastSetUrl = articleUrl;
  107.                 currentWrapUrl = articleUrl;
  108.             }
  109.         });
  110.  
  111.         tocContainer.appendChild(a);
  112.         globalHeadingIndex++;
  113.     });
  114.  
  115.     /* -------- Подсветка active заголовка -------- */
  116.     function activateHeadingOnScroll() {
  117.         const links = Array.from(tocContainer.querySelectorAll(".articles-link"));
  118.         let headingElements = links.map(link => {
  119.             const id = link.dataset.anchor;
  120.             return wrap.querySelector(`#${CSS.escape(id)}`);
  121.         }).filter(Boolean);
  122.  
  123.         let currentActiveIndex = -1;
  124.  
  125.         function updateActive() {
  126.             const scrollPos = window.scrollY + 100;
  127.  
  128.             let newIndex = -1;
  129.             headingElements.forEach((heading, i) => {
  130.                 const top = heading.getBoundingClientRect().top + window.scrollY;
  131.                 if (top <= scrollPos) newIndex = i;
  132.             });
  133.  
  134.             if (newIndex === currentActiveIndex) return;
  135.  
  136.             currentActiveIndex = newIndex;
  137.  
  138.             links.forEach((link, i) => {
  139.                 link.classList.toggle("active", i === newIndex);
  140.             });
  141.         }
  142.  
  143.         window.addEventListener("scroll", updateActive, { passive: true });
  144.  
  145.         window.addEventListener("resize", () => {
  146.             headingElements = links.map(link => {
  147.                 const id = link.dataset.anchor;
  148.                 return wrap.querySelector(`#${CSS.escape(id)}`);
  149.             }).filter(Boolean);
  150.  
  151.             updateActive();
  152.         });
  153.  
  154.         updateActive();
  155.     }
  156.  
  157.     activateHeadingOnScroll();
  158.  
  159.     wrap.dataset.tocReady = "1";
  160. }
  161.  
  162. /* ==========================================
  163.    Инициализация TOC на уже существующих блоках
  164.    ========================================== */
  165. document.querySelectorAll(".article-wrapper").forEach(wrap => {
  166.     initTOCForWrap(wrap);
  167. });
  168.  
  169. /* ==========================================
  170.    Lazyload: MutationObserver
  171.    ========================================== */
  172. const observer = new MutationObserver(mutations => {
  173.     for (const m of mutations) {
  174.         m.addedNodes.forEach(node => {
  175.             if (node.nodeType !== 1) return;
  176.  
  177.             // Новый wrap добавился
  178.             if (node.matches(".article-wrapper")) {
  179.                 initTOCForWrap(node);
  180.             }
  181.  
  182.             // Или внутри добавился wrap
  183.             const inner = node.querySelectorAll?.(".article-wrapper");
  184.             inner?.forEach(wrap => initTOCForWrap(wrap));
  185.  
  186.             // обновляем активный wrap после вставки контента
  187.             requestAnimationFrame(updateActiveWrap);
  188.         });
  189.     }
  190. });
  191.  
  192. observer.observe(document.body, {
  193.     childList: true,
  194.     subtree: true
  195. });
  196.  
  197. /* ==========================================
  198.    Запуск отслеживания активного WRAP
  199.    ========================================== */
  200. window.addEventListener("scroll", () => {
  201.     requestAnimationFrame(updateActiveWrap);
  202. }, { passive: true });
  203.  
  204. window.addEventListener("resize", updateActiveWrap);
  205. window.addEventListener("load", updateActiveWrap);
  206.  
  207. })();
  208. </script>
Advertisement
Add Comment
Please, Sign In to add comment