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

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

当前为 2021-10-30 提交的版本,查看 最新版本

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