Toc Bar, 自动生成文章大纲。知乎、微信公众号等阅读好伴侣

[改了一下,支持多一点网站]自动生成文章大纲目录,在页面右侧展示一个浮动的组件。覆盖常用在线阅读资讯站(技术向)。github/medium/MDN/掘金/简书等

  1. // ==UserScript==
  2. // @name Toc Bar, auto-generating table of content
  3. // @name:zh-CN Toc Bar, 自动生成文章大纲。知乎、微信公众号等阅读好伴侣
  4. // @author hikerpig
  5. // @namespace https://github.com/hikerpig
  6. // @license MIT
  7. // @description A floating table of content widget
  8. // @description:zh-CN [改了一下,支持多一点网站]自动生成文章大纲目录,在页面右侧展示一个浮动的组件。覆盖常用在线阅读资讯站(技术向)。github/medium/MDN/掘金/简书等
  9. // @version 1.9.6
  10. // @match *://www.jianshu.com/p/*
  11. // @match *://cdn2.jianshu.io/p/*
  12. // @match *://zhuanlan.zhihu.com/p/*
  13. // @match *://www.zhihu.com/pub/reader/*
  14. // @match *://mp.weixin.qq.com/s*
  15. // @match *://cnodejs.org/topic/*
  16. // @match *://*zcfy.cc/article/*
  17. // @match *://juejin.cn/post/*
  18. // @match *://juejin.cn/book/*
  19. // @match *://dev.to/*/*
  20. // @exclude *://dev.to/settings/*
  21. // @match *://web.dev/*
  22. // @match *://medium.com/*
  23. // @exclude *://medium.com/media/*
  24. // @match *://itnext.io/*
  25. // @match *://python-patterns.guide/*
  26. // @match *://www.mysqltutorial.org/*
  27. // @match *://en.wikipedia.org/*
  28. // @match *://vuejs.org/*
  29. // @match *://docs.python.org/*
  30. // @match *://packaging.python.org/*
  31. // @match *://*.readthedocs.io/*
  32. // @match *://docs.djangoproject.com/*
  33. // @match *://www.cnblogs.com/*
  34. // @match *://bigsearcher.com/*
  35. // @match *://ffmpeg.org/*
  36. // @match *://www.ruanyifeng.com/*
  37. // @match *://stackoverflow.blog/*
  38. // @match *://realpython.com/*
  39. // @match *://www.infoq.cn/article/*
  40. // @match *://towardsdatascience.com/*
  41. // @match *://hackernoon.com/*
  42. // @match *://css-tricks.com/*
  43. // @match *://www.smashingmagazine.com/*/*
  44. // @match *://distill.pub/*
  45. // @match *://github.com/*/*
  46. // @match *://github.com/*/issues/*
  47. // @match *://developer.mozilla.org/*/docs/*
  48. // @match *://learning.oreilly.com/library/view/*
  49. // @match *://developer.chrome.com/extensions/*
  50. // @match *://app.getpocket.com/read/*
  51. // @match *://indepth.dev/posts/*
  52. // @match *://gitlab.com/*
  53. // @match *://10.0.68.37/*
  54. // @match *://10.0.34.61/*
  55. // @run-at document-idle
  56. // @grant GM_getResourceText
  57. // @grant GM_addStyle
  58. // @grant GM_setValue
  59. // @grant GM_getValue
  60. // @require https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.18.2/tocbot.min.js
  61. // @icon https://raw.githubusercontent.com/hikerpig/toc-bar-userscript/master/toc-logo.svg
  62. // @homepageURL https://github.com/hikerpig/toc-bar-userscript
  63. // ==/UserScript==
  64.  
  65. (function () {
  66. /**
  67. * @typedef {Object} SiteSetting
  68. * @property {string} contentSelector
  69. * @property {string} siteName
  70. * @property {Object} style
  71. * @property {Number} scrollSmoothOffset
  72. * @property {Number} initialTop
  73. * @property {Number} headingsOffset
  74. * @property {() => Boolean} shouldShow
  75. * @property {(ele) => HTMLElement} findHeaderId
  76. * @property {(e) => void} onClick
  77. * @property {(tocBar: TocBar) => void} onInit
  78. */
  79.  
  80. /** @type {{[key: string]: Partial<SiteSetting>}} */
  81. const SITE_SETTINGS = {
  82. jianshu: {
  83. contentSelector: '.ouvJEz',
  84. style: {
  85. top: '55px',
  86. color: '#ea6f5a',
  87. },
  88. },
  89. 'zhuanlan.zhihu.com': {
  90. contentSelector: 'article',
  91. scrollSmoothOffset: -52,
  92. shouldShow() {
  93. return location.pathname.startsWith('/p/')
  94. },
  95. },
  96. 'www.zhihu.com': {
  97. contentSelector: '.reader-chapter-content',
  98. scrollSmoothOffset: -52,
  99. },
  100. zcfy: {
  101. contentSelector: '.markdown-body',
  102. },
  103. qq: {
  104. contentSelector: '.rich_media_content',
  105. },
  106. 'juejin.cn': function() {
  107. let contentSelector = '.article' // post
  108. if (/\/book\//.test(location.pathname)) {
  109. contentSelector = '.book-body'
  110. }
  111. return {
  112. contentSelector,
  113. }
  114. },
  115. 'dev.to': {
  116. contentSelector: 'article',
  117. scrollSmoothOffset: -56,
  118. shouldShow() {
  119. return ['/search', '/top/'].every(s => !location.pathname.startsWith(s))
  120. },
  121. },
  122. 'medium.com': {
  123. contentSelector: 'article'
  124. },
  125. 'docs.djangoproject.com': {
  126. contentSelector: '#docs-content'
  127. },
  128. 'hackernoon.com': {
  129. contentSelector: 'main',
  130. scrollSmoothOffset: -80,
  131. },
  132. 'towardsdatascience.com': {
  133. contentSelector: 'article'
  134. },
  135. 'css-tricks.com': {
  136. contentSelector: 'main'
  137. },
  138. 'distill.pub': {
  139. contentSelector: 'body'
  140. },
  141. 'smashingmagazine': {
  142. contentSelector: 'article'
  143. },
  144. 'web.dev': {
  145. contentSelector: '#content'
  146. },
  147. 'python-patterns.guide': {
  148. contentSelector: '.section',
  149. },
  150. 'www.mysqltutorial.org': {
  151. contentSelector: 'article',
  152. },
  153. 'github.com': function () {
  154. const README_SEL = '.entry-content'
  155. const WIKI_CONTENT_SEL = '#wiki-body'
  156. const ISSUE_CONTENT_SEL = '.comment .comment-body'
  157.  
  158. const matchedSel = [README_SEL, ISSUE_CONTENT_SEL, WIKI_CONTENT_SEL].find((sel) => {
  159. const c = document.querySelector(sel)
  160. if (c) {
  161. return true
  162. }
  163. })
  164.  
  165. if (!matchedSel) {
  166. return {
  167. contentSelect: false,
  168. }
  169. }
  170.  
  171. const isIssueDetail = /\/issues\//.test(location.pathname)
  172. const ISSUE_DETAIL_HEADING_OFFSET = 60
  173.  
  174. /** Ugly hack for github issues */
  175. const onClick = isIssueDetail ? function (e) {
  176. const href = e.target.getAttribute('href')
  177. const header = document.body.querySelector(href)
  178. if (header) {
  179. const rect = header.getBoundingClientRect()
  180. const currentWindowScrollTop = document.documentElement.scrollTop
  181. const scrollY = rect.y + currentWindowScrollTop - ISSUE_DETAIL_HEADING_OFFSET
  182.  
  183. window.scrollTo(0, scrollY)
  184.  
  185. location.hash = href
  186.  
  187. e.preventDefault()
  188. e.stopPropagation()
  189. }
  190. }: null
  191.  
  192. return {
  193. siteName: 'github.com',
  194. contentSelector: matchedSel,
  195. hasInnerContainers: isIssueDetail ? true: false,
  196. scrollSmoothOffset: isIssueDetail ? -ISSUE_DETAIL_HEADING_OFFSET: 0,
  197. headingsOffset: isIssueDetail ? ISSUE_DETAIL_HEADING_OFFSET: 0,
  198. initialTop: 500,
  199. onClick,
  200. findHeaderId(ele) {
  201. let id
  202. let anchor = ele.querySelector('.anchor')
  203. if (anchor) id = anchor.getAttribute('id')
  204.  
  205. if (!anchor) {
  206. anchor = ele.querySelector('a')
  207. if (anchor) id = anchor.hash.replace(/^#/, '')
  208. }
  209. return id
  210. },
  211. }
  212. },
  213. 'developer.mozilla.org': {
  214. contentSelector: '#content',
  215. onInit() {
  216. setTimeout(() => {
  217. tocbot.refresh()
  218. }, 2000)
  219. }
  220. },
  221. 'learning.oreilly.com': {
  222. contentSelector: '#sbo-rt-content'
  223. },
  224. 'developer.chrome.com': {
  225. contentSelector: 'article'
  226. },
  227. 'www.infoq.cn': {
  228. contentSelector: '.article-main',
  229. scrollSmoothOffset: -107
  230. },
  231. 'app.getpocket.com': {
  232. contentSelector: '[role=main]',
  233. },
  234. 'indepth.dev': {
  235. contentSelector: '.content',
  236. },
  237. 'gitlab.com': {
  238. contentSelector: '.file-content',
  239. scrollSmoothOffset: -40
  240. },
  241. 'docs.celeryproject.org': {
  242. contentSelector: '[role=main]',
  243. },
  244. 'docs.python.org': {
  245. contentSelector: '[role=main]',
  246. },
  247. 'packaging.python.org': {
  248. contentSelector: '[role=main]',
  249. },
  250. 'readthedocs.io': {
  251. contentSelector: '[role=main]',
  252. },
  253. 'bigsearcher.com': {
  254. contentSelector: 'body',
  255. },
  256. 'ffmpeg.org': {
  257. contentSelector: '#page-content-wrapper',
  258. },
  259. 'www.ruanyifeng.com': {
  260. contentSelector: 'article',
  261. },
  262. 'realpython.com': {
  263. contentSelector: '.main-content',
  264. },
  265. 'en.wikipedia.org': {
  266. contentSelector: '#content',
  267. },
  268. 'www.cnblogs.com': {
  269. contentSelector: '#main',
  270. },
  271. 'stackoverflow.blog': {
  272. contentSelector: 'article',
  273. },
  274. 'vuejs.org': {
  275. contentSelector: 'main > div',
  276. },
  277. '10.0.68.37': {
  278. contentSelector: '.file-content',
  279. scrollSmoothOffset: -40
  280. },
  281. '10.0.34.61': {
  282. contentSelector: '.file-content',
  283. scrollSmoothOffset: -40
  284. },
  285. }
  286.  
  287. function getSiteInfo() {
  288. let siteName
  289. if (SITE_SETTINGS[location.hostname]) {
  290. siteName = location.hostname;
  291. } else if (location.hostname.indexOf('readthedocs.io') > -1) {
  292. siteName = 'readthedocs.io';
  293. } else {
  294. const match = location.href.match(
  295. /([\d\w]+)\.(com|cn|net|org|im|io|cc|site|tv)/i
  296. )
  297. siteName = match ? match[1] : null
  298. }
  299. if (siteName && SITE_SETTINGS[siteName]) {
  300. return {
  301. siteName,
  302. siteSetting: SITE_SETTINGS[siteName],
  303. }
  304. }
  305. }
  306.  
  307. function getPageTocOptions() {
  308. let siteInfo = getSiteInfo()
  309. if (siteInfo) {
  310. if (typeof siteInfo.siteSetting === 'function') {
  311. return siteInfo.siteSetting()
  312. }
  313.  
  314. let siteSetting = { ...siteInfo.siteSetting }
  315. if (siteSetting.shouldShow && !siteSetting.shouldShow()) {
  316. return
  317. }
  318. if (typeof siteSetting.contentSelector === 'function') {
  319. const contentSelector = siteSetting.contentSelector()
  320. if (!contentSelector) return
  321. siteSetting = {...siteSetting, contentSelector}
  322. }
  323. if (typeof siteSetting.scrollSmoothOffset === 'function') {
  324. siteSetting.scrollSmoothOffset = siteSetting.scrollSmoothOffset()
  325. }
  326.  
  327. console.log('[toc-bar] found site info for', siteInfo.siteName)
  328. return siteSetting
  329. }
  330. }
  331.  
  332. function guessThemeColor() {
  333. const meta = document.head.querySelector('meta[name="theme-color"]')
  334. if (meta) {
  335. return meta.getAttribute('content')
  336. }
  337. }
  338.  
  339. /**
  340. * @param {String} content
  341. * @return {String}
  342. */
  343. function doContentHash(content) {
  344. const val = content.split('').reduce((prevHash, currVal) => (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
  345. return val.toString(32)
  346. }
  347.  
  348. const POSITION_STORAGE = {
  349. cache: null,
  350. checkCache() {
  351. if (!POSITION_STORAGE.cache) {
  352. POSITION_STORAGE.cache = GM_getValue('tocbar-positions', {})
  353. }
  354. },
  355. get(k) {
  356. k = k || location.host
  357. POSITION_STORAGE.checkCache()
  358. return POSITION_STORAGE.cache[k]
  359. },
  360. set(k, position) {
  361. k = k || location.host
  362. POSITION_STORAGE.checkCache()
  363. POSITION_STORAGE.cache[k] = position
  364. GM_setValue('tocbar-positions', POSITION_STORAGE.cache)
  365. },
  366. }
  367.  
  368. function isEmpty(input) {
  369. if (input) {
  370. return Object.keys(input).length === 0
  371. }
  372. return true
  373. }
  374.  
  375. /** 宽度,也用于计算拖动时的最小 right */
  376. const TOC_BAR_WIDTH = 340
  377.  
  378. const TOC_BAR_DEFAULT_ACTIVE_COLOR = '#54BC4B';
  379.  
  380. // ---------------- TocBar ----------------------
  381. const TOC_BAR_STYLE = `
  382. .toc-bar {
  383. --toc-bar-active-color: ${TOC_BAR_DEFAULT_ACTIVE_COLOR};
  384. --toc-bar-text-color: #333;
  385. --toc-bar-background-color: #FEFEFE;
  386.  
  387. position: fixed;
  388. z-index: 9000;
  389. right: 5px;
  390. top: 80px;
  391. width: ${TOC_BAR_WIDTH}px;
  392. font-size: 14px;
  393. box-sizing: border-box;
  394. padding: 0 10px 10px 0;
  395. box-shadow: 0 1px 3px #DDD;
  396. border-radius: 4px;
  397. transition: width 0.2s ease;
  398. color: var(--toc-bar-text-color);
  399. background: var(--toc-bar-background-color);
  400.  
  401. user-select:none;
  402. -moz-user-select:none;
  403. -webkit-user-select: none;
  404. -ms-user-select: none;
  405. }
  406.  
  407. .toc-bar[colorscheme="dark"] {
  408. --toc-bar-text-color: #fafafa;
  409. --toc-bar-background-color: #333;
  410. }
  411. .toc-bar[colorscheme="dark"] svg {
  412. fill: var(--toc-bar-text-color);
  413. stroke: var(--toc-bar-text-color);
  414. }
  415.  
  416. .toc-bar.toc-bar--collapsed {
  417. width: 30px;
  418. height: 30px;
  419. padding: 0;
  420. overflow: hidden;
  421. }
  422.  
  423. .toc-bar--collapsed .toc {
  424. display: none;
  425. }
  426.  
  427. .toc-bar--collapsed .hidden-when-collapsed {
  428. display: none;
  429. }
  430.  
  431. .toc-bar__header {
  432. font-weight: bold;
  433. padding-bottom: 5px;
  434. display: flex;
  435. justify-content: space-between;
  436. align-items: center;
  437. cursor: move;
  438. }
  439.  
  440. .toc-bar__refresh {
  441. position: relative;
  442. top: -2px;
  443. }
  444.  
  445. .toc-bar__icon-btn {
  446. height: 1em;
  447. width: 1em;
  448. cursor: pointer;
  449. transition: transform 0.2s ease;
  450. }
  451.  
  452. .toc-bar__icon-btn:hover {
  453. opacity: 0.7;
  454. }
  455.  
  456. .toc-bar__icon-btn svg {
  457. max-width: 100%;
  458. max-height: 100%;
  459. vertical-align: top;
  460. }
  461.  
  462. .toc-bar__actions {
  463. align-items: center;
  464. }
  465. .toc-bar__actions .toc-bar__icon-btn {
  466. margin-left: 1em;
  467. }
  468.  
  469. .toc-bar__scheme {
  470. transform: translateY(-1px) scale(1.1);
  471. }
  472.  
  473. .toc-bar__header-left {
  474. align-items: center;
  475. }
  476.  
  477. .toc-bar__toggle {
  478. cursor: pointer;
  479. padding: 8px 8px;
  480. box-sizing: content-box;
  481. transition: transform 0.2s ease;
  482. }
  483.  
  484. .toc-bar__title {
  485. margin-left: 5px;
  486. }
  487.  
  488. .toc-bar a.toc-link {
  489. overflow: hidden;
  490. text-overflow: ellipsis;
  491. white-space: nowrap;
  492. display: block;
  493. line-height: 1.6;
  494. }
  495.  
  496. .flex {
  497. display: flex;
  498. }
  499.  
  500. /* tocbot related */
  501. .toc-bar__toc {
  502. max-height: 80vh;
  503. overflow-y: auto;
  504. }
  505.  
  506. .toc-list-item > a:hover {
  507. text-decoration: underline;
  508. }
  509.  
  510. .toc-list {
  511. padding-inline-start: 0;
  512. }
  513.  
  514. .toc-bar__toc > .toc-list {
  515. margin: 0;
  516. overflow: hidden;
  517. position: relative;
  518. padding-left: 5px;
  519. }
  520.  
  521. .toc-bar__toc>.toc-list li {
  522. list-style: none;
  523. padding-left: 8px;
  524. position: static;
  525. }
  526.  
  527. a.toc-link {
  528. color: currentColor;
  529. height: 100%;
  530. }
  531.  
  532. .is-collapsible {
  533. max-height: 1000px;
  534. overflow: hidden;
  535. transition: all 300ms ease-in-out;
  536. }
  537.  
  538. .is-collapsed {
  539. max-height: 0;
  540. }
  541.  
  542. .is-position-fixed {
  543. position: fixed !important;
  544. top: 0;
  545. }
  546.  
  547. .is-active-link {
  548. font-weight: 700;
  549. }
  550.  
  551. .toc-link::before {
  552. background-color: var(--toc-bar-background-color);
  553. content: ' ';
  554. display: inline-block;
  555. height: inherit;
  556. left: 0;
  557. margin-top: -1px;
  558. position: absolute;
  559. width: 2px;
  560. }
  561.  
  562. .is-active-link::before {
  563. background-color: var(--toc-bar-active-color);
  564. }
  565.  
  566. .toc-list-item,
  567. .toc-link {
  568. font-size: 1em; /* reset font size */
  569. }
  570.  
  571.  
  572. @media print {
  573. .toc-bar__no-print { display: none !important; }
  574. }
  575. /* end tocbot related */
  576. `
  577.  
  578. const TOCBOT_CONTAINTER_CLASS = 'toc-bar__toc'
  579.  
  580. const DARKMODE_KEY = 'tocbar-darkmode'
  581.  
  582. /**
  583. * @typedef {Object} TocBarOptions
  584. * @property {String} [siteName]
  585. * @property {Number} [initialTop]
  586. */
  587.  
  588. /**
  589. * @class
  590. * @param {TocBarOptions} options
  591. */
  592. function TocBar(options={}) {
  593. this.options = options
  594.  
  595. // inject style
  596. GM_addStyle(TOC_BAR_STYLE)
  597.  
  598. this.element = document.createElement('div')
  599. this.element.id = 'toc-bar'
  600. this.element.classList.add('toc-bar', 'toc-bar__no-print')
  601. document.body.appendChild(this.element)
  602.  
  603. /** @type {Boolean} */
  604. this.visible = true
  605.  
  606. this.initHeader()
  607.  
  608. // create a container tocbot
  609. const tocElement = document.createElement('div')
  610. this.tocElement = tocElement
  611. tocElement.classList.add(TOCBOT_CONTAINTER_CLASS)
  612. this.element.appendChild(tocElement)
  613.  
  614. POSITION_STORAGE.checkCache()
  615. const cachedPosition = POSITION_STORAGE.get(options.siteName)
  616. if (!isEmpty(cachedPosition)) {
  617. this.element.style.top = `${Math.max(0, cachedPosition.top)}px`
  618. this.element.style.right = `${cachedPosition.right}px`
  619. } else if (options.hasOwnProperty('initialTop')) {
  620. this.element.style.top = `${options.initialTop}px`
  621. }
  622.  
  623. if (GM_getValue('tocbar-hidden', false)) {
  624. this.toggle(false)
  625. }
  626.  
  627. const isDark = Boolean(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
  628. /** @type {Boolean} */
  629. this.isDarkMode = isDark
  630.  
  631. if (GM_getValue(DARKMODE_KEY, false)) {
  632. this.toggleScheme(true)
  633. }
  634. }
  635.  
  636. 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>`
  637.  
  638. const TOC_ICON = `
  639. <?xml version="1.0" encoding="utf-8"?>
  640. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
  641. viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
  642. <g>
  643. <g>
  644. <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
  645. v132.5h32V166.5h287.8v24.9H553.8v82.8h114.1H693h225.6V114.5L835.2,45.9z M806.2,93.2H134.2V67.2h672.1v26.1H806.2z"/>
  646. <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
  647. 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 "/>
  648. </g>
  649. </g>
  650. </svg>
  651. `
  652.  
  653. const LIGHT_ICON = `
  654. <?xml version="1.0" encoding="iso-8859-1"?>
  655. <!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
  656. <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
  657. <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"
  658. viewBox="0 0 181.328 181.328" style="enable-background:new 0 0 181.328 181.328;" xml:space="preserve" style="transform: translateY(-1px);">
  659. <g>
  660. <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
  661. 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
  662. 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
  663. 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
  664. 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"/>
  665. <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
  666. C37.237,90.831,40.595,87.473,40.595,83.331z"/>
  667. <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
  668. C181.328,79.189,177.97,75.831,173.828,75.831z"/>
  669. <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
  670. 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"/>
  671. <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
  672. 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"/>
  673. <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
  674. 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"
  675. />
  676. <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
  677. 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
  678. C127.532,49.391,129.452,50.123,131.371,50.123z"/>
  679. <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
  680. C98.164,136.758,94.806,133.4,90.664,133.4z"/>
  681. </g>
  682. </svg>
  683. `
  684.  
  685. TocBar.prototype = {
  686. /**
  687. * @method TocBar
  688. */
  689. initHeader() {
  690. const header = document.createElement('div')
  691. header.classList.add('toc-bar__header')
  692. header.innerHTML = `
  693. <div class="flex toc-bar__header-left">
  694. <div class="toc-bar__toggle toc-bar__icon-btn" title="Toggle TOC Bar">
  695. ${TOC_ICON}
  696. </div>
  697. <div class="toc-bar__title hidden-when-collapsed">TOC Bar</div>
  698. </div>
  699. <div class="toc-bar__actions flex hidden-when-collapsed">
  700. <div class="toc-bar__scheme toc-bar__icon-btn" title="Toggle Light/Dark Mode">
  701. ${LIGHT_ICON}
  702. </div>
  703. <div class="toc-bar__refresh toc-bar__icon-btn" title="Refresh TOC">
  704. ${REFRESH_ICON}
  705. </div>
  706. </div>
  707. `
  708. const toggleElement = header.querySelector('.toc-bar__toggle')
  709. toggleElement.addEventListener('click', () => {
  710. this.toggle()
  711. GM_setValue('tocbar-hidden', !this.visible)
  712. })
  713. this.logoSvg = toggleElement.querySelector('svg')
  714.  
  715. const refreshElement = header.querySelector('.toc-bar__refresh')
  716. refreshElement.addEventListener('click', () => {
  717. try {
  718. tocbot.refresh()
  719. } catch (error) {
  720. console.warn('error in tocbot.refresh', error)
  721. }
  722. })
  723.  
  724. const toggleSchemeElement = header.querySelector('.toc-bar__scheme')
  725. toggleSchemeElement.addEventListener('click', () => {
  726. this.toggleScheme()
  727. })
  728. // ---------------- header drag ----------------------
  729. const dragState = {
  730. startMouseX: 0,
  731. startMouseY: 0,
  732. startPositionX: 0,
  733. startPositionY: 0,
  734. startElementDisToRight: 0,
  735. isDragging: false,
  736. curRight: 0,
  737. curTop: 0,
  738. }
  739.  
  740. const onMouseMove = (e) => {
  741. if (!dragState.isDragging) return
  742. const deltaX = e.pageX - dragState.startMouseX
  743. const deltaY = e.pageY - dragState.startMouseY
  744. // 要换算为 right 数字
  745. const newRight = Math.max(30 - TOC_BAR_WIDTH, dragState.startElementDisToRight - deltaX)
  746. const newTop = Math.max(0, dragState.startPositionY + deltaY)
  747. Object.assign(dragState, {
  748. curTop: newTop,
  749. curRight: newRight,
  750. })
  751. // console.table({ newRight, newTop})
  752. this.element.style.right = `${newRight}px`
  753. this.element.style.top = `${newTop}px`
  754. }
  755.  
  756. const onMouseUp = () => {
  757. Object.assign(dragState, {
  758. isDragging: false,
  759. })
  760. document.body.removeEventListener('mousemove', onMouseMove)
  761. document.body.removeEventListener('mouseup', onMouseUp)
  762.  
  763. POSITION_STORAGE.set(this.options.siteName, {
  764. top: dragState.curTop,
  765. right: dragState.curRight,
  766. })
  767. }
  768.  
  769. header.addEventListener('mousedown', (e) => {
  770. if (e.target === toggleElement) return
  771. const bbox = this.element.getBoundingClientRect()
  772. Object.assign(dragState, {
  773. isDragging: true,
  774. startMouseX: e.pageX,
  775. startMouseY: e.pageY,
  776. startPositionX: bbox.x,
  777. startPositionY: bbox.y,
  778. startElementDisToRight: document.body.clientWidth - bbox.right,
  779. })
  780. document.body.addEventListener('mousemove', onMouseMove)
  781. document.body.addEventListener('mouseup', onMouseUp)
  782. })
  783. // ----------------end header drag -------------------
  784.  
  785. this.element.appendChild(header)
  786. },
  787. /**
  788. * @method TocBar
  789. * @param {SiteSetting} options
  790. */
  791. initTocbot(options) {
  792. const me = this
  793.  
  794. /**
  795. * records for existing ids to prevent id conflict (when there are headings of same content)
  796. * @type {Object} {[key: string]: number}
  797. **/
  798. this._tocContentCountCache = {}
  799.  
  800. const tocbotOptions = Object.assign(
  801. {},
  802. {
  803. tocSelector: `.${TOCBOT_CONTAINTER_CLASS}`,
  804. scrollSmoothOffset: options.scrollSmoothOffset || 0,
  805. headingObjectCallback(obj, ele) {
  806. // if there is no id on the header element, add one that derived from hash of header title
  807. // remove ¶ and # notation in headers text
  808. obj.textContent = obj.textContent.replace(/¶|#/g, '');
  809. if (!ele.id) {
  810. let newId
  811. if (options.findHeaderId) {
  812. newId = options.findHeaderId(ele)
  813. }
  814. if (!newId) {
  815. newId = me.generateHeaderId(obj, ele)
  816. ele.setAttribute('id', newId)
  817. }
  818. if (newId) obj.id = newId
  819. }
  820. return obj
  821. },
  822. headingSelector: 'h1, h2, h3, h4, h5',
  823. collapseDepth: 4,
  824. },
  825. options
  826. )
  827. // console.log('tocbotOptions', tocbotOptions);
  828. try {
  829. tocbot.init(tocbotOptions)
  830. if (options.onInit) {
  831. options.onInit(this)
  832. }
  833. } catch (error) {
  834. console.warn('error in tocbot.init', error)
  835. }
  836. },
  837. generateHeaderId(obj, ele) {
  838. const hash = doContentHash(obj.textContent)
  839. let count = 1
  840. let resultHash = hash
  841. if (this._tocContentCountCache[hash]) {
  842. count = this._tocContentCountCache[hash] + 1
  843. resultHash = doContentHash(`${hash}-${count}`)
  844. }
  845. this._tocContentCountCache[hash] = count
  846. return `tocbar-${resultHash}`
  847. },
  848. /**
  849. * @method TocBar
  850. */
  851. toggle(shouldShow = !this.visible) {
  852. const HIDDEN_CLASS = 'toc-bar--collapsed'
  853. const LOGO_HIDDEN_CLASS = 'toc-logo--collapsed'
  854. if (shouldShow) {
  855. this.element.classList.remove(HIDDEN_CLASS)
  856. this.logoSvg && this.logoSvg.classList.remove(LOGO_HIDDEN_CLASS)
  857. } else {
  858. this.element.classList.add(HIDDEN_CLASS)
  859. this.logoSvg && this.logoSvg.classList.add(LOGO_HIDDEN_CLASS)
  860.  
  861. const right = parseInt(this.element.style.right)
  862. if (right && right < 0) {
  863. this.element.style.right = "0px"
  864. const cachedPosition = POSITION_STORAGE.cache
  865. if (!isEmpty(cachedPosition)) {
  866. POSITION_STORAGE.set(null, {...cachedPosition, right: 0 })
  867. }
  868. }
  869. }
  870. this.visible = shouldShow
  871. },
  872. /**
  873. * Toggle light/dark scheme
  874. * @method TocBar
  875. */
  876. toggleScheme(isDark) {
  877. const isDarkMode = typeof isDark === 'undefined' ? !this.isDarkMode: isDark
  878. this.element.setAttribute('colorscheme', isDarkMode ? 'dark': 'light')
  879. console.log('[toc-bar] toggle scheme', isDarkMode)
  880. this.isDarkMode = isDarkMode
  881.  
  882. GM_setValue(DARKMODE_KEY, isDarkMode)
  883. this.refreshStyle()
  884. },
  885. refreshStyle() {
  886. const themeColor = guessThemeColor()
  887. if (themeColor && !this.isDarkMode) {
  888. this.element.style.setProperty('--toc-bar-active-color', themeColor);
  889. } else if (this.isDarkMode) {
  890. this.element.style.setProperty('--toc-bar-active-color', TOC_BAR_DEFAULT_ACTIVE_COLOR);
  891. }
  892. },
  893. }
  894. // ----------------end TocBar -------------------
  895.  
  896. function main() {
  897. const options = getPageTocOptions()
  898.  
  899. if (options) {
  900. const tocBar = new TocBar(options)
  901. tocBar.initTocbot(options)
  902. tocBar.refreshStyle()
  903. }
  904.  
  905. }
  906. //main();
  907. setTimeout(main, 350);
  908. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址