Toc Bar

在页面右侧展示一个浮动的文章大纲目录

当前为 2020-07-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Toc Bar
  3. // @author hikerpig
  4. // @namespace https://github.com/hikerpig
  5. // @license MIT
  6. // @description A floating table of content widget
  7. // @description:zh-CN 在页面右侧展示一个浮动的文章大纲目录
  8. // @version 1.2.0
  9. // @match *://www.jianshu.com/p/*
  10. // @match *://cdn2.jianshu.io/p/*
  11. // @match *://zhuanlan.zhihu.com/p/*
  12. // @match *://mp.weixin.qq.com/s*
  13. // @match *://cnodejs.org/topic/*
  14. // @match *://*zcfy.cc/article/*
  15. // @match *://juejin.im/entry/*
  16. // @match *://dev.to/*
  17. // @match *://web.dev/*
  18. // @match *://medium.com/*
  19. // @match *://css-tricks.com/*
  20. // @match *://www.smashingmagazine.com/*/*
  21. // @match *://distill.pub/*
  22. // @match *://github.com/*/*
  23. // @match *://developer.mozilla.org/*/docs/*
  24. // @run-at document-idle
  25. // @grant GM_getResourceText
  26. // @grant GM_addStyle
  27. // @require https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.11.1/tocbot.min.js
  28. // @homepageURL https://github.com/hikerpig/toc-bar-userscript
  29. // ==/UserScript==
  30.  
  31. (function () {
  32. const SITE_SETTINGS = {
  33. jianshu: {
  34. contentSelector: '.ouvJEz',
  35. style: {
  36. top: '55px',
  37. color: '#ea6f5a',
  38. },
  39. },
  40. 'zhuanlan.zhihu.com': {
  41. contentSelector: 'article',
  42. scrollSmoothOffset: -52,
  43. shouldShow() {
  44. return location.pathname.startsWith('/p/')
  45. },
  46. },
  47. zcfy: {
  48. contentSelector: '.markdown-body',
  49. },
  50. qq: {
  51. contentSelector: '.rich_media_content',
  52. },
  53. 'juejin.im': {
  54. contentSelector: '.entry-public-main',
  55. },
  56. 'dev.to': {
  57. contentSelector: 'article',
  58. scrollSmoothOffset: -56,
  59. shouldShow() {
  60. return ['/search', '/top/'].every(s => !location.pathname.startsWith(s))
  61. },
  62. },
  63. 'medium.com': {
  64. contentSelector: 'article'
  65. },
  66. 'css-tricks.com': {
  67. contentSelector: 'main'
  68. },
  69. 'distill.pub': {
  70. contentSelector: 'body'
  71. },
  72. 'smashingmagazine': {
  73. contentSelector: 'article'
  74. },
  75. 'web.dev': {
  76. contentSelector: '#content'
  77. },
  78. 'github.com': {
  79. contentSelector() {
  80. const README_SEL = '#readme'
  81. const WIKI_CONTENT_SEL = '.repository-content'
  82. const matchedSel = [README_SEL, WIKI_CONTENT_SEL].find((sel) => {
  83. return !!document.querySelector(README_SEL)
  84. })
  85.  
  86. if (matchedSel) return matchedSel
  87.  
  88. return false
  89. }
  90. },
  91. 'developer.mozilla.org': {
  92. contentSelector: '#content'
  93. }
  94. }
  95.  
  96. function getSiteInfo() {
  97. let siteName
  98. if (SITE_SETTINGS[location.hostname]) {
  99. siteName = location.hostname
  100. } else {
  101. const match = location.href.match(
  102. /([\d\w]+)\.(com|cn|net|org|im|io|cc|site|tv)/i
  103. )
  104. siteName = match ? match[1] : null
  105. }
  106. if (siteName && SITE_SETTINGS[siteName]) {
  107. return {
  108. siteName,
  109. siteSetting: SITE_SETTINGS[siteName],
  110. }
  111. }
  112. }
  113.  
  114. function getPageTocOptions() {
  115. let siteInfo = getSiteInfo()
  116. if (siteInfo) {
  117. let siteSetting = siteInfo.siteSetting
  118. if (siteSetting.shouldShow && !siteSetting.shouldShow()) {
  119. return
  120. }
  121. if (typeof siteSetting.contentSelector === 'function') {
  122. const contentSelector = siteSetting.contentSelector()
  123. if (!contentSelector) return
  124. siteSetting = {...siteSetting, contentSelector}
  125. }
  126. console.log('[toc-bar] found site info for', siteInfo.siteName)
  127. return siteSetting
  128. }
  129. }
  130.  
  131.  
  132. function guessThemeColor() {
  133. const meta = document.head.querySelector('meta[name="theme-color"]')
  134. if (meta) {
  135. return meta.getAttribute('content')
  136. }
  137. }
  138.  
  139. /**
  140. * @param {String} content
  141. * @return {String}
  142. */
  143. function doContentHash(content) {
  144. const val = content.split('').reduce((prevHash, currVal) => (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
  145. return val.toString(32)
  146. }
  147.  
  148. // ---------------- TocBar ----------------------
  149. const TOC_BAR_STYLE = `
  150. .toc-bar {
  151. --toc-bar-active-color: #54BC4B;
  152.  
  153. position: fixed;
  154. z-index: 9000;
  155. right: 5px;
  156. top: 80px;
  157. width: 340px;
  158. font-size: 14px;
  159. box-sizing: border-box;
  160. padding: 0 10px 10px 0;
  161. box-shadow: 0 1px 3px #DDD;
  162. border-radius: 4px;
  163. transition: width 0.2s ease;
  164. color: #333;
  165. background: #FEFEFE;
  166. }
  167.  
  168. .toc-bar.toc-bar--collapsed {
  169. width: 30px;
  170. height: 30px;
  171. padding: 0;
  172. overflow: hidden;
  173. }
  174.  
  175. .toc-bar--collapsed .toc {
  176. display: none;
  177. }
  178.  
  179. .toc-bar--collapsed .hidden-when-collapsed {
  180. display: none;
  181. }
  182.  
  183. .toc-bar__header {
  184. font-weight: bold;
  185. padding-bottom: 5px;
  186. display: flex;
  187. justify-content: space-between;
  188. align-items: center;
  189. cursor: move;
  190. }
  191.  
  192. .toc-bar__refresh {
  193. position: relative;
  194. top: -2px;
  195. }
  196.  
  197. .toc-bar__icon-btn {
  198. height: 1em;
  199. width: 1em;
  200. cursor: pointer;
  201. transition: transform 0.2s ease;
  202. }
  203.  
  204. .toc-bar__icon-btn:hover {
  205. opacity: 0.7;
  206. }
  207.  
  208. .toc-bar__icon-btn svg {
  209. max-width: 100%;
  210. max-height: 100%;
  211. vertical-align: top;
  212. }
  213.  
  214. .toc-bar__header-left {
  215. align-items: center;
  216. }
  217.  
  218. .toc-bar__toggle {
  219. cursor: pointer;
  220. padding: 8px 8px;
  221. box-sizing: content-box;
  222. transition: transform 0.2s ease;
  223. }
  224.  
  225. .toc-bar__title {
  226. margin-left: 5px;
  227. }
  228.  
  229. .toc-bar a.toc-link {
  230. overflow: hidden;
  231. text-overflow: ellipsis;
  232. white-space: nowrap;
  233. display: block;
  234. line-height: 1.6;
  235. }
  236.  
  237. .flex {
  238. display: flex;
  239. }
  240.  
  241. /* tocbot related */
  242. .toc-bar__toc {
  243. max-height: 80vh;
  244. overflow-y: scroll;
  245. }
  246.  
  247. .toc-list-item > a:hover {
  248. text-decoration: underline;
  249. }
  250.  
  251. .toc-list {
  252. padding-inline-start: 0;
  253. }
  254.  
  255. .toc-bar__toc > .toc-list {
  256. margin: 0;
  257. overflow: hidden;
  258. position: relative;
  259. padding-left: 5px;
  260. }
  261.  
  262. .toc-bar__toc>.toc-list li {
  263. list-style: none;
  264. padding-left: 8px;
  265. position: static;
  266. }
  267.  
  268. a.toc-link {
  269. color: currentColor;
  270. height: 100%;
  271. }
  272.  
  273. .is-collapsible {
  274. max-height: 1000px;
  275. overflow: hidden;
  276. transition: all 300ms ease-in-out;
  277. }
  278.  
  279. .is-collapsed {
  280. max-height: 0;
  281. }
  282.  
  283. .is-position-fixed {
  284. position: fixed !important;
  285. top: 0;
  286. }
  287.  
  288. .is-active-link {
  289. font-weight: 700;
  290. }
  291.  
  292. .toc-link::before {
  293. background-color: #EEE;
  294. content: ' ';
  295. display: inline-block;
  296. height: inherit;
  297. left: 0;
  298. margin-top: -1px;
  299. position: absolute;
  300. width: 2px;
  301. }
  302.  
  303. .is-active-link::before {
  304. background-color: var(--toc-bar-active-color);
  305. }
  306. /* end tocbot related */
  307. `
  308.  
  309. const TOCBOT_CONTAINTER_CLASS = 'toc-bar__toc'
  310.  
  311. /**
  312. * @class
  313. */
  314. function TocBar() {
  315. // inject style
  316. GM_addStyle(TOC_BAR_STYLE)
  317.  
  318. this.element = document.createElement('div')
  319. this.element.id = 'toc-bar'
  320. this.element.classList.add('toc-bar')
  321. document.body.appendChild(this.element)
  322.  
  323. /** @type {Boolean} */
  324. this.visible = true
  325.  
  326. this.initHeader()
  327.  
  328. // create a container tocbot
  329. const tocElement = document.createElement('div')
  330. this.tocElement = tocElement
  331. tocElement.classList.add(TOCBOT_CONTAINTER_CLASS)
  332. this.element.appendChild(tocElement)
  333. }
  334.  
  335. 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>`
  336.  
  337. const TOC_ICON = `
  338. <?xml version="1.0" encoding="utf-8"?>
  339. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
  340. viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
  341. <g>
  342. <g>
  343. <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
  344. v132.5h32V166.5h287.8v24.9H553.8v82.8h114.1H693h225.6V114.5L835.2,45.9z M806.2,93.2H134.2V67.2h672.1v26.1H806.2z"/>
  345. <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
  346. 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 "/>
  347. </g>
  348. </g>
  349. </svg>
  350. `
  351.  
  352. TocBar.prototype = {
  353. /**
  354. * @method TocBar
  355. */
  356. initHeader() {
  357. const header = document.createElement('div')
  358. header.classList.add('toc-bar__header')
  359. header.innerHTML = `
  360. <div class="flex toc-bar__header-left">
  361. <div class="toc-bar__toggle toc-bar__icon-btn" title="Toggle TOC Bar">
  362. ${TOC_ICON}
  363. </div>
  364. <div class="toc-bar__title hidden-when-collapsed">TOC Bar</div>
  365. </div>
  366. <div class="toc-bar__actions hidden-when-collapsed">
  367. <div class="toc-bar__refresh toc-bar__icon-btn" title="Refresh TOC">
  368. ${REFRESH_ICON}
  369. </div>
  370. </div>
  371. `
  372. const toggleElement = header.querySelector('.toc-bar__toggle')
  373. toggleElement.addEventListener('click', () => {
  374. this.toggle()
  375. })
  376. this.logoSvg = toggleElement.querySelector('svg')
  377.  
  378. const refreshElement = header.querySelector('.toc-bar__refresh')
  379. refreshElement.addEventListener('click', () => {
  380. tocbot.refresh()
  381. })
  382. // ---------------- header drag ----------------------
  383. const dragState = {
  384. startMouseX: 0,
  385. startMouseY: 0,
  386. startPositionX: 0,
  387. startPositionY: 0,
  388. startElementDisToRight: 0,
  389. isDragging: false,
  390. }
  391.  
  392. const onMouseMove = (e) => {
  393. if (!dragState.isDragging) return
  394. const deltaX = e.pageX - dragState.startMouseX
  395. const deltaY = e.pageY - dragState.startMouseY
  396. // 要换算为 right 数字
  397. const newRight = dragState.startElementDisToRight - deltaX
  398. const newTop = dragState.startPositionY + deltaY
  399. // console.table({ newRight, newTop})
  400. this.element.style.right = `${newRight}px`
  401. this.element.style.top = `${newTop}px`
  402. }
  403.  
  404. const onMouseUp = (e) => {
  405. Object.assign(dragState, {
  406. isDragging: false,
  407. })
  408. document.body.removeEventListener('mousemove', onMouseMove)
  409. document.body.removeEventListener('mouseup', onMouseUp)
  410. }
  411.  
  412. header.addEventListener('mousedown', (e) => {
  413. if (e.target === toggleElement) return
  414. const bbox = this.element.getBoundingClientRect()
  415. Object.assign(dragState, {
  416. isDragging: true,
  417. startMouseX: e.pageX,
  418. startMouseY: e.pageY,
  419. startPositionX: bbox.x,
  420. startPositionY: bbox.y,
  421. startElementDisToRight: document.body.clientWidth - bbox.right,
  422. })
  423. document.body.addEventListener('mousemove', onMouseMove)
  424. document.body.addEventListener('mouseup', onMouseUp)
  425. })
  426. // ----------------end header drag -------------------
  427.  
  428. this.element.appendChild(header)
  429. },
  430. /**
  431. * @method TocBar
  432. */
  433. initTocbot(options) {
  434. const me = this
  435. const tocbotOptions = Object.assign(
  436. {},
  437. {
  438. tocSelector: `.${TOCBOT_CONTAINTER_CLASS}`,
  439. scrollSmoothOffset: options.scrollSmoothOffset || 0,
  440. // hasInnerContainers: true,
  441. headingObjectCallback(obj, ele) {
  442. // if there is no id on the header element, add one that derived from hash of header title
  443. if (!ele.id) {
  444. const newId = me.generateHeaderId(obj, ele)
  445. ele.setAttribute('id', newId)
  446. obj.id = newId
  447. }
  448. return obj
  449. },
  450. headingSelector: 'h1, h2, h3, h4, h5',
  451. collapseDepth: 4,
  452. },
  453. options
  454. )
  455. // console.log('tocbotOptions', tocbotOptions);
  456. tocbot.init(tocbotOptions)
  457. },
  458. generateHeaderId(obj, ele) {
  459. return `tocbar-${doContentHash(obj.textContent)}`
  460. },
  461. /**
  462. * @method TocBar
  463. */
  464. toggle(shouldShow = !this.visible) {
  465. const HIDDEN_CLASS = 'toc-bar--collapsed'
  466. const LOGO_HIDDEN_CLASS = 'toc-logo--collapsed'
  467. if (shouldShow) {
  468. this.element.classList.remove(HIDDEN_CLASS)
  469. this.logoSvg && this.logoSvg.classList.remove(LOGO_HIDDEN_CLASS)
  470. } else {
  471. this.element.classList.add(HIDDEN_CLASS)
  472. this.logoSvg && this.logoSvg.classList.add(LOGO_HIDDEN_CLASS)
  473. }
  474. this.visible = shouldShow
  475. },
  476. refreshStyle() {
  477. const themeColor = guessThemeColor()
  478. if (themeColor) {
  479. this.element.style.setProperty('--toc-bar-active-color', themeColor);
  480. }
  481. },
  482. }
  483. // ----------------end TocBar -------------------
  484.  
  485. function main() {
  486. const options = getPageTocOptions()
  487. if (options) {
  488.  
  489. const tocBar = new TocBar()
  490. tocBar.initTocbot(options)
  491. tocBar.refreshStyle()
  492. }
  493. }
  494.  
  495. main()
  496. })()

QingJ © 2025

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