// ==UserScript== // @name Notion Sticky TOC (2022 Available) // @name:zh-CN Notion 固定左侧 TOC (2022 亲测可用) // @namespace https://github.com/soraliu // @version 0.5.0 // @description Set Notion TOC Sticky. // @description:zh-cn TOC 左侧固定 // @author Sora Liu // @match https://www.notion.so/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== /* jshint esversion:6 */ (function() { 'use strict'; // selectors const SELECTOR_NOTION_APP = 'notion-app'; const SELECTOR_NOTION_SCROLLER = '.notion-scroller'; const SELECTOR_NOTION_FRAME = '.notion-frame'; const SELECTOR_NOTION_TOC = '.notion-table_of_contents-block'; const SELECTOR_MODAL_PAGE = '.notion-peek-renderer'; // the selector which used to check if the page is in any modal // toc config const TOC_CONFIG_OFFSET_TOP = '128px'; const TOC_CONFIG_LEFT = '64px'; const TOC_CONFIG_WIDTH = '168px'; /* Helper function to wait for the element ready */ const waitFor = (...selectors) => new Promise(resolve => { const delay = 500; const f = () => { const elements = selectors.map(selector => document.querySelector(selector)); if (elements.every(element => element != null)) { resolve(elements); } else { setTimeout(f, delay); } }; f(); }); // for performance const LISTENED_SELECTORS = new WeakMap(); const addScrollListener = (selectors, fn) => { let lastKnownScrollPosition = 0; let ticking = false; fn(lastKnownScrollPosition); // init once selectors.forEach(selector => { if (LISTENED_SELECTORS.has(selector)) { return; } // set listened LISTENED_SELECTORS.set(selector, true); selector.addEventListener('scroll', function(e) { lastKnownScrollPosition = window.scrollY; if (!ticking) { window.requestAnimationFrame(function() { fn(lastKnownScrollPosition); ticking = false; }); ticking = true; } }, false); }); }; const setTopOffsetByEle = ({ stickyEle, offsetEle, offsetTop }) => { stickyEle.style.top = `calc(${offsetEle.offsetTop * 2 - offsetEle.getBoundingClientRect().top}px + ${offsetTop})`; }; const callback = function(mutations) { waitFor(SELECTOR_NOTION_TOC).then(([el]) => { const toc = document.querySelector(SELECTOR_NOTION_TOC); const modal = document.querySelector(SELECTOR_MODAL_PAGE); if (!modal && toc) { const frame = toc.closest(SELECTOR_NOTION_FRAME); const toc_p = toc.parentElement; toc.style.zIndex = '999'; toc.style.position = 'absolute'; toc.style.left = TOC_CONFIG_LEFT; toc.style.width = `${(frame.getBoundingClientRect().width - toc_p.getBoundingClientRect().width) / 2}px`; addScrollListener([toc.closest(SELECTOR_NOTION_SCROLLER)], scroll => { setTopOffsetByEle({ stickyEle: toc, offsetEle: toc_p, offsetTop: TOC_CONFIG_OFFSET_TOP }); }); } }); }; const observer = new MutationObserver(callback); observer.observe(document.getElementById(SELECTOR_NOTION_APP), { childList: true, subtree: true } ); })();