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

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

当前为 2022-04-13 提交的版本,查看 最新版本

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