您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动生成文章大纲目录,在页面右侧展示一个浮动的组件。覆盖常用在线阅读资讯站(技术向)。github/medium/MDN/掘金/简书等
// ==UserScript== // @name Toc Bar, auto-generating table of content // @name:zh-CN Toc Bar, 自动生成文章大纲。知乎、微信公众号等阅读好伴侣 // @author hikerpig // @namespace https://github.com/hikerpig // @license MIT // @description A floating table of content widget // @description:zh-CN 自动生成文章大纲目录,在页面右侧展示一个浮动的组件。覆盖常用在线阅读资讯站(技术向)。github/medium/MDN/掘金/简书等 // @version 1.9.6 // @match *://www.jianshu.com/p/* // @match *://cdn2.jianshu.io/p/* // @match *://zhuanlan.zhihu.com/p/* // @match *://www.zhihu.com/pub/reader/* // @match *://mp.weixin.qq.com/s* // @match *://cnodejs.org/topic/* // @match *://*zcfy.cc/article/* // @match *://juejin.cn/post/* // @match *://juejin.cn/book/* // @match *://dev.to/*/* // @exclude *://dev.to/settings/* // @match *://web.dev/* // @match *://medium.com/* // @exclude *://medium.com/media/* // @match *://itnext.io/* // @match *://python-patterns.guide/* // @match *://www.mysqltutorial.org/* // @match *://en.wikipedia.org/* // @match *://vuejs.org/* // @match *://docs.python.org/* // @match *://packaging.python.org/* // @match *://*.readthedocs.io/* // @match *://docs.djangoproject.com/* // @match *://www.cnblogs.com/* // @match *://bigsearcher.com/* // @match *://ffmpeg.org/* // @match *://www.ruanyifeng.com/* // @match *://stackoverflow.blog/* // @match *://realpython.com/* // @match *://www.infoq.cn/article/* // @match *://towardsdatascience.com/* // @match *://hackernoon.com/* // @match *://css-tricks.com/* // @match *://www.smashingmagazine.com/*/* // @match *://distill.pub/* // @match *://github.com/*/* // @match *://github.com/*/issues/* // @match *://developer.mozilla.org/*/docs/* // @match *://learning.oreilly.com/library/view/* // @match *://developer.chrome.com/extensions/* // @match *://app.getpocket.com/read/* // @match *://indepth.dev/posts/* // @match *://gitlab.com/* // @run-at document-idle // @grant GM_getResourceText // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @require https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.18.2/tocbot.min.js // @icon https://raw.githubusercontent.com/hikerpig/toc-bar-userscript/master/toc-logo.svg // @homepageURL https://github.com/hikerpig/toc-bar-userscript // ==/UserScript== (function () { /** * @typedef {Object} SiteSetting * @property {string} contentSelector * @property {string} siteName * @property {Object} style * @property {Number} scrollSmoothOffset * @property {Number} initialTop * @property {Number} headingsOffset * @property {() => Boolean} shouldShow * @property {(ele) => HTMLElement} findHeaderId * @property {(e) => void} onClick * @property {(tocBar: TocBar) => void} onInit */ /** @type {{[key: string]: Partial<SiteSetting>}} */ const SITE_SETTINGS = { jianshu: { contentSelector: '.ouvJEz', style: { top: '55px', color: '#ea6f5a', }, }, 'zhuanlan.zhihu.com': { contentSelector: 'article', scrollSmoothOffset: -52, shouldShow() { return location.pathname.startsWith('/p/') }, }, 'www.zhihu.com': { contentSelector: '.reader-chapter-content', scrollSmoothOffset: -52, }, zcfy: { contentSelector: '.markdown-body', }, qq: { contentSelector: '.rich_media_content', }, 'juejin.cn': function() { let contentSelector = '.article' // post if (/\/book\//.test(location.pathname)) { contentSelector = '.book-body' } return { contentSelector, } }, 'dev.to': { contentSelector: 'article', scrollSmoothOffset: -56, shouldShow() { return ['/search', '/top/'].every(s => !location.pathname.startsWith(s)) }, }, 'medium.com': { contentSelector: 'article' }, 'docs.djangoproject.com': { contentSelector: '#docs-content' }, 'hackernoon.com': { contentSelector: 'main', scrollSmoothOffset: -80, }, 'towardsdatascience.com': { contentSelector: 'article' }, 'css-tricks.com': { contentSelector: 'main' }, 'distill.pub': { contentSelector: 'body' }, 'smashingmagazine': { contentSelector: 'article' }, 'web.dev': { contentSelector: '#content' }, 'python-patterns.guide': { contentSelector: '.section', }, 'www.mysqltutorial.org': { contentSelector: 'article', }, 'github.com': function () { const README_SEL = '.entry-content' const WIKI_CONTENT_SEL = '#wiki-body' const ISSUE_CONTENT_SEL = '.comment .comment-body' const matchedSel = [README_SEL, ISSUE_CONTENT_SEL, WIKI_CONTENT_SEL].find((sel) => { const c = document.querySelector(sel) if (c) { return true } }) if (!matchedSel) { return { contentSelect: false, } } const isIssueDetail = /\/issues\//.test(location.pathname) const ISSUE_DETAIL_HEADING_OFFSET = 60 /** Ugly hack for github issues */ const onClick = isIssueDetail ? function (e) { const href = e.target.getAttribute('href') const header = document.body.querySelector(href) if (header) { const rect = header.getBoundingClientRect() const currentWindowScrollTop = document.documentElement.scrollTop const scrollY = rect.y + currentWindowScrollTop - ISSUE_DETAIL_HEADING_OFFSET window.scrollTo(0, scrollY) location.hash = href e.preventDefault() e.stopPropagation() } }: null return { siteName: 'github.com', contentSelector: matchedSel, hasInnerContainers: isIssueDetail ? true: false, scrollSmoothOffset: isIssueDetail ? -ISSUE_DETAIL_HEADING_OFFSET: 0, headingsOffset: isIssueDetail ? ISSUE_DETAIL_HEADING_OFFSET: 0, initialTop: 500, onClick, findHeaderId(ele) { let id let anchor = ele.querySelector('.anchor') if (anchor) id = anchor.getAttribute('id') if (!anchor) { anchor = ele.querySelector('a') if (anchor) id = anchor.hash.replace(/^#/, '') } return id }, } }, 'developer.mozilla.org': { contentSelector: '#content', onInit() { setTimeout(() => { tocbot.refresh() }, 2000) } }, 'learning.oreilly.com': { contentSelector: '#sbo-rt-content' }, 'developer.chrome.com': { contentSelector: 'article' }, 'www.infoq.cn': { contentSelector: '.article-main', scrollSmoothOffset: -107 }, 'app.getpocket.com': { contentSelector: '[role=main]', }, 'indepth.dev': { contentSelector: '.content', }, 'gitlab.com': { contentSelector: '.file-content', scrollSmoothOffset: -40 }, 'docs.celeryproject.org': { contentSelector: '[role=main]', }, 'docs.python.org': { contentSelector: '[role=main]', }, 'packaging.python.org': { contentSelector: '[role=main]', }, 'readthedocs.io': { contentSelector: '[role=main]', }, 'bigsearcher.com': { contentSelector: 'body', }, 'ffmpeg.org': { contentSelector: '#page-content-wrapper', }, 'www.ruanyifeng.com': { contentSelector: 'article', }, 'realpython.com': { contentSelector: '.main-content', }, 'en.wikipedia.org': { contentSelector: '#content', }, 'www.cnblogs.com': { contentSelector: '#main', }, 'stackoverflow.blog': { contentSelector: 'article', }, 'vuejs.org': { contentSelector: 'main > div', }, } function getSiteInfo() { let siteName if (SITE_SETTINGS[location.hostname]) { siteName = location.hostname; } else if (location.hostname.indexOf('readthedocs.io') > -1) { siteName = 'readthedocs.io'; } else { const match = location.href.match( /([\d\w]+)\.(com|cn|net|org|im|io|cc|site|tv)/i ) siteName = match ? match[1] : null } if (siteName && SITE_SETTINGS[siteName]) { return { siteName, siteSetting: SITE_SETTINGS[siteName], } } } function getPageTocOptions() { let siteInfo = getSiteInfo() if (siteInfo) { if (typeof siteInfo.siteSetting === 'function') { return siteInfo.siteSetting() } let siteSetting = { ...siteInfo.siteSetting } if (siteSetting.shouldShow && !siteSetting.shouldShow()) { return } if (typeof siteSetting.contentSelector === 'function') { const contentSelector = siteSetting.contentSelector() if (!contentSelector) return siteSetting = {...siteSetting, contentSelector} } if (typeof siteSetting.scrollSmoothOffset === 'function') { siteSetting.scrollSmoothOffset = siteSetting.scrollSmoothOffset() } console.log('[toc-bar] found site info for', siteInfo.siteName) return siteSetting } } function guessThemeColor() { const meta = document.head.querySelector('meta[name="theme-color"]') if (meta) { return meta.getAttribute('content') } } /** * @param {String} content * @return {String} */ function doContentHash(content) { const val = content.split('').reduce((prevHash, currVal) => (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0); return val.toString(32) } const POSITION_STORAGE = { cache: null, checkCache() { if (!POSITION_STORAGE.cache) { POSITION_STORAGE.cache = GM_getValue('tocbar-positions', {}) } }, get(k) { k = k || location.host POSITION_STORAGE.checkCache() return POSITION_STORAGE.cache[k] }, set(k, position) { k = k || location.host POSITION_STORAGE.checkCache() POSITION_STORAGE.cache[k] = position GM_setValue('tocbar-positions', POSITION_STORAGE.cache) }, } function isEmpty(input) { if (input) { return Object.keys(input).length === 0 } return true } /** 宽度,也用于计算拖动时的最小 right */ const TOC_BAR_WIDTH = 340 const TOC_BAR_DEFAULT_ACTIVE_COLOR = '#54BC4B'; // ---------------- TocBar ---------------------- const TOC_BAR_STYLE = ` .toc-bar { --toc-bar-active-color: ${TOC_BAR_DEFAULT_ACTIVE_COLOR}; --toc-bar-text-color: #333; --toc-bar-background-color: #FEFEFE; position: fixed; z-index: 9000; right: 5px; top: 80px; width: ${TOC_BAR_WIDTH}px; font-size: 14px; box-sizing: border-box; padding: 0 10px 10px 0; box-shadow: 0 1px 3px #DDD; border-radius: 4px; transition: width 0.2s ease; color: var(--toc-bar-text-color); background: var(--toc-bar-background-color); user-select:none; -moz-user-select:none; -webkit-user-select: none; -ms-user-select: none; } .toc-bar[colorscheme="dark"] { --toc-bar-text-color: #fafafa; --toc-bar-background-color: #333; } .toc-bar[colorscheme="dark"] svg { fill: var(--toc-bar-text-color); stroke: var(--toc-bar-text-color); } .toc-bar.toc-bar--collapsed { width: 30px; height: 30px; padding: 0; overflow: hidden; } .toc-bar--collapsed .toc { display: none; } .toc-bar--collapsed .hidden-when-collapsed { display: none; } .toc-bar__header { font-weight: bold; padding-bottom: 5px; display: flex; justify-content: space-between; align-items: center; cursor: move; } .toc-bar__refresh { position: relative; top: -2px; } .toc-bar__icon-btn { height: 1em; width: 1em; cursor: pointer; transition: transform 0.2s ease; } .toc-bar__icon-btn:hover { opacity: 0.7; } .toc-bar__icon-btn svg { max-width: 100%; max-height: 100%; vertical-align: top; } .toc-bar__actions { align-items: center; } .toc-bar__actions .toc-bar__icon-btn { margin-left: 1em; } .toc-bar__scheme { transform: translateY(-1px) scale(1.1); } .toc-bar__header-left { align-items: center; } .toc-bar__toggle { cursor: pointer; padding: 8px 8px; box-sizing: content-box; transition: transform 0.2s ease; } .toc-bar__title { margin-left: 5px; } .toc-bar a.toc-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; line-height: 1.6; } .flex { display: flex; } /* tocbot related */ .toc-bar__toc { max-height: 80vh; overflow-y: auto; } .toc-list-item > a:hover { text-decoration: underline; } .toc-list { padding-inline-start: 0; } .toc-bar__toc > .toc-list { margin: 0; overflow: hidden; position: relative; padding-left: 5px; } .toc-bar__toc>.toc-list li { list-style: none; padding-left: 8px; position: static; } a.toc-link { color: currentColor; height: 100%; } .is-collapsible { max-height: 1000px; overflow: hidden; transition: all 300ms ease-in-out; } .is-collapsed { max-height: 0; } .is-position-fixed { position: fixed !important; top: 0; } .is-active-link { font-weight: 700; } .toc-link::before { background-color: var(--toc-bar-background-color); content: ' '; display: inline-block; height: inherit; left: 0; margin-top: -1px; position: absolute; width: 2px; } .is-active-link::before { background-color: var(--toc-bar-active-color); } .toc-list-item, .toc-link { font-size: 1em; /* reset font size */ } @media print { .toc-bar__no-print { display: none !important; } } /* end tocbot related */ ` const TOCBOT_CONTAINTER_CLASS = 'toc-bar__toc' const DARKMODE_KEY = 'tocbar-darkmode' /** * @typedef {Object} TocBarOptions * @property {String} [siteName] * @property {Number} [initialTop] */ /** * @class * @param {TocBarOptions} options */ function TocBar(options={}) { this.options = options // inject style GM_addStyle(TOC_BAR_STYLE) this.element = document.createElement('div') this.element.id = 'toc-bar' this.element.classList.add('toc-bar', 'toc-bar__no-print') document.body.appendChild(this.element) /** @type {Boolean} */ this.visible = true this.initHeader() // create a container tocbot const tocElement = document.createElement('div') this.tocElement = tocElement tocElement.classList.add(TOCBOT_CONTAINTER_CLASS) this.element.appendChild(tocElement) POSITION_STORAGE.checkCache() const cachedPosition = POSITION_STORAGE.get(options.siteName) if (!isEmpty(cachedPosition)) { this.element.style.top = `${Math.max(0, cachedPosition.top)}px` this.element.style.right = `${cachedPosition.right}px` } else if (options.hasOwnProperty('initialTop')) { this.element.style.top = `${options.initialTop}px` } if (GM_getValue('tocbar-hidden', false)) { this.toggle(false) } const isDark = Boolean(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); /** @type {Boolean} */ this.isDarkMode = isDark if (GM_getValue(DARKMODE_KEY, false)) { this.toggleScheme(true) } } const REFRESH_ICON = `<svg t="1593614403764" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5002" width="200" height="200"><path d="M918 702.8 918 702.8c45.6-98.8 52-206 26-303.6-30-112.4-104-212.8-211.6-273.6L780 23.2l-270.8 70.8 121.2 252.4 50-107.6c72.8 44.4 122.8 114.4 144 192.8 18.8 70.8 14.4 147.6-18.8 219.6-42 91.2-120.8 153.6-210.8 177.6-13.2 3.6-26.4 6-39.6 8l56 115.6c5.2-1.2 10.4-2.4 16-4C750.8 915.2 860 828.8 918 702.8L918 702.8M343.2 793.2c-74-44.4-124.8-114.8-146-194-18.8-70.8-14.4-147.6 18.8-219.6 42-91.2 120.8-153.6 210.8-177.6 14.8-4 30-6.8 45.6-8.8l-55.6-116c-7.2 1.6-14.8 3.2-22 5.2-124 33.2-233.6 119.6-291.2 245.6-45.6 98.8-52 206-26 303.2l0 0.4c30.4 113.2 105.2 214 213.6 274.8l-45.2 98 270.4-72-122-252L343.2 793.2 343.2 793.2M343.2 793.2 343.2 793.2z" p-id="5003"></path></svg>` const TOC_ICON = ` <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve"> <g> <g> <path d="M835.2,45.9H105.2v166.8l93.2,61.5h115.8H356h30.6v-82.8H134.2v-24.9h286.2v107.6h32.2V141.6H134.2V118h672.1v23.6H486.4 v132.5h32V166.5h287.8v24.9H553.8v82.8h114.1H693h225.6V114.5L835.2,45.9z M806.2,93.2H134.2V67.2h672.1v26.1H806.2z"/> <polygon points="449.3,1008.2 668,1008.2 668,268.9 553.8,268.9 553.8,925.4 518.4,925.4 518.4,268.9 486.4,268.9 486.4,925.4 452.6,925.4 452.6,268.9 420.4,268.9 420.4,925.4 386.6,925.4 386.6,268.9 356,268.9 356,946.7 "/> </g> </g> </svg> ` const LIGHT_ICON = ` <?xml version="1.0" encoding="iso-8859-1"?> <!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 181.328 181.328" style="enable-background:new 0 0 181.328 181.328;" xml:space="preserve" style="transform: translateY(-1px);"> <g> <path d="M118.473,46.308V14.833c0-4.142-3.358-7.5-7.5-7.5H70.357c-4.142,0-7.5,3.358-7.5,7.5v31.474 C51.621,54.767,44.34,68.214,44.34,83.331c0,25.543,20.781,46.324,46.324,46.324s46.324-20.781,46.324-46.324 C136.988,68.215,129.708,54.769,118.473,46.308z M77.857,22.333h25.615v16.489c-4.071-1.174-8.365-1.815-12.809-1.815 c-4.443,0-8.736,0.642-12.807,1.814V22.333z M90.664,114.655c-17.273,0-31.324-14.052-31.324-31.324 c0-17.272,14.052-31.324,31.324-31.324s31.324,14.052,31.324,31.324C121.988,100.604,107.937,114.655,90.664,114.655z"/> <path d="M40.595,83.331c0-4.142-3.358-7.5-7.5-7.5H7.5c-4.142,0-7.5,3.358-7.5,7.5c0,4.142,3.358,7.5,7.5,7.5h25.595 C37.237,90.831,40.595,87.473,40.595,83.331z"/> <path d="M173.828,75.831h-25.595c-4.142,0-7.5,3.358-7.5,7.5c0,4.142,3.358,7.5,7.5,7.5h25.595c4.142,0,7.5-3.358,7.5-7.5 C181.328,79.189,177.97,75.831,173.828,75.831z"/> <path d="M44.654,47.926c1.464,1.465,3.384,2.197,5.303,2.197c1.919,0,3.839-0.732,5.303-2.197c2.929-2.929,2.929-7.678,0-10.606 L37.162,19.222c-2.929-2.93-7.678-2.929-10.606,0c-2.929,2.929-2.929,7.678,0,10.606L44.654,47.926z"/> <path d="M136.674,118.735c-2.93-2.929-7.678-2.928-10.607,0c-2.929,2.929-2.928,7.678,0,10.607l18.1,18.098 c1.465,1.464,3.384,2.196,5.303,2.196c1.919,0,3.839-0.732,5.304-2.197c2.929-2.929,2.928-7.678,0-10.607L136.674,118.735z"/> <path d="M44.654,118.736l-18.099,18.098c-2.929,2.929-2.929,7.677,0,10.607c1.464,1.465,3.384,2.197,5.303,2.197 c1.919,0,3.839-0.732,5.303-2.197l18.099-18.098c2.929-2.929,2.929-7.677,0-10.606C52.332,115.807,47.583,115.807,44.654,118.736z" /> <path d="M131.371,50.123c1.919,0,3.839-0.732,5.303-2.196l18.1-18.098c2.929-2.929,2.929-7.678,0-10.607 c-2.929-2.928-7.678-2.929-10.607-0.001l-18.1,18.098c-2.929,2.929-2.929,7.678,0,10.607 C127.532,49.391,129.452,50.123,131.371,50.123z"/> <path d="M90.664,133.4c-4.142,0-7.5,3.358-7.5,7.5v25.595c0,4.142,3.358,7.5,7.5,7.5c4.142,0,7.5-3.358,7.5-7.5V140.9 C98.164,136.758,94.806,133.4,90.664,133.4z"/> </g> </svg> ` TocBar.prototype = { /** * @method TocBar */ initHeader() { const header = document.createElement('div') header.classList.add('toc-bar__header') header.innerHTML = ` <div class="flex toc-bar__header-left"> <div class="toc-bar__toggle toc-bar__icon-btn" title="Toggle TOC Bar"> ${TOC_ICON} </div> <div class="toc-bar__title hidden-when-collapsed">TOC Bar</div> </div> <div class="toc-bar__actions flex hidden-when-collapsed"> <div class="toc-bar__scheme toc-bar__icon-btn" title="Toggle Light/Dark Mode"> ${LIGHT_ICON} </div> <div class="toc-bar__refresh toc-bar__icon-btn" title="Refresh TOC"> ${REFRESH_ICON} </div> </div> ` const toggleElement = header.querySelector('.toc-bar__toggle') toggleElement.addEventListener('click', () => { this.toggle() GM_setValue('tocbar-hidden', !this.visible) }) this.logoSvg = toggleElement.querySelector('svg') const refreshElement = header.querySelector('.toc-bar__refresh') refreshElement.addEventListener('click', () => { try { tocbot.refresh() } catch (error) { console.warn('error in tocbot.refresh', error) } }) const toggleSchemeElement = header.querySelector('.toc-bar__scheme') toggleSchemeElement.addEventListener('click', () => { this.toggleScheme() }) // ---------------- header drag ---------------------- const dragState = { startMouseX: 0, startMouseY: 0, startPositionX: 0, startPositionY: 0, startElementDisToRight: 0, isDragging: false, curRight: 0, curTop: 0, } const onMouseMove = (e) => { if (!dragState.isDragging) return const deltaX = e.pageX - dragState.startMouseX const deltaY = e.pageY - dragState.startMouseY // 要换算为 right 数字 const newRight = Math.max(30 - TOC_BAR_WIDTH, dragState.startElementDisToRight - deltaX) const newTop = Math.max(0, dragState.startPositionY + deltaY) Object.assign(dragState, { curTop: newTop, curRight: newRight, }) // console.table({ newRight, newTop}) this.element.style.right = `${newRight}px` this.element.style.top = `${newTop}px` } const onMouseUp = () => { Object.assign(dragState, { isDragging: false, }) document.body.removeEventListener('mousemove', onMouseMove) document.body.removeEventListener('mouseup', onMouseUp) POSITION_STORAGE.set(this.options.siteName, { top: dragState.curTop, right: dragState.curRight, }) } header.addEventListener('mousedown', (e) => { if (e.target === toggleElement) return const bbox = this.element.getBoundingClientRect() Object.assign(dragState, { isDragging: true, startMouseX: e.pageX, startMouseY: e.pageY, startPositionX: bbox.x, startPositionY: bbox.y, startElementDisToRight: document.body.clientWidth - bbox.right, }) document.body.addEventListener('mousemove', onMouseMove) document.body.addEventListener('mouseup', onMouseUp) }) // ----------------end header drag ------------------- this.element.appendChild(header) }, /** * @method TocBar * @param {SiteSetting} options */ initTocbot(options) { const me = this /** * records for existing ids to prevent id conflict (when there are headings of same content) * @type {Object} {[key: string]: number} **/ this._tocContentCountCache = {} const tocbotOptions = Object.assign( {}, { tocSelector: `.${TOCBOT_CONTAINTER_CLASS}`, scrollSmoothOffset: options.scrollSmoothOffset || 0, headingObjectCallback(obj, ele) { // if there is no id on the header element, add one that derived from hash of header title // remove ¶ and # notation in headers text obj.textContent = obj.textContent.replace(/¶|#/g, ''); if (!ele.id) { let newId if (options.findHeaderId) { newId = options.findHeaderId(ele) } if (!newId) { newId = me.generateHeaderId(obj, ele) ele.setAttribute('id', newId) } if (newId) obj.id = newId } return obj }, headingSelector: 'h1, h2, h3, h4, h5', collapseDepth: 4, }, options ) // console.log('tocbotOptions', tocbotOptions); try { tocbot.init(tocbotOptions) if (options.onInit) { options.onInit(this) } } catch (error) { console.warn('error in tocbot.init', error) } }, generateHeaderId(obj, ele) { const hash = doContentHash(obj.textContent) let count = 1 let resultHash = hash if (this._tocContentCountCache[hash]) { count = this._tocContentCountCache[hash] + 1 resultHash = doContentHash(`${hash}-${count}`) } this._tocContentCountCache[hash] = count return `tocbar-${resultHash}` }, /** * @method TocBar */ toggle(shouldShow = !this.visible) { const HIDDEN_CLASS = 'toc-bar--collapsed' const LOGO_HIDDEN_CLASS = 'toc-logo--collapsed' if (shouldShow) { this.element.classList.remove(HIDDEN_CLASS) this.logoSvg && this.logoSvg.classList.remove(LOGO_HIDDEN_CLASS) } else { this.element.classList.add(HIDDEN_CLASS) this.logoSvg && this.logoSvg.classList.add(LOGO_HIDDEN_CLASS) const right = parseInt(this.element.style.right) if (right && right < 0) { this.element.style.right = "0px" const cachedPosition = POSITION_STORAGE.cache if (!isEmpty(cachedPosition)) { POSITION_STORAGE.set(null, {...cachedPosition, right: 0 }) } } } this.visible = shouldShow }, /** * Toggle light/dark scheme * @method TocBar */ toggleScheme(isDark) { const isDarkMode = typeof isDark === 'undefined' ? !this.isDarkMode: isDark this.element.setAttribute('colorscheme', isDarkMode ? 'dark': 'light') console.log('[toc-bar] toggle scheme', isDarkMode) this.isDarkMode = isDarkMode GM_setValue(DARKMODE_KEY, isDarkMode) this.refreshStyle() }, refreshStyle() { const themeColor = guessThemeColor() if (themeColor && !this.isDarkMode) { this.element.style.setProperty('--toc-bar-active-color', themeColor); } else if (this.isDarkMode) { this.element.style.setProperty('--toc-bar-active-color', TOC_BAR_DEFAULT_ACTIVE_COLOR); } }, } // ----------------end TocBar ------------------- function main() { const options = getPageTocOptions() if (options) { const tocBar = new TocBar(options) tocBar.initTocbot(options) tocBar.refreshStyle() } } main() })()