Toc Bar, 文章大纲

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

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

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

QingJ © 2025

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