Toc Bar

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

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

  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.0.3
  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 *://www.zcfy.cc/article/*
  15. // @match *://dev.to/*
  16. // @match *://web.dev/*
  17. // @match *://medium.com/*
  18. // @match *://css-tricks.com/*
  19. // @match *://www.smashingmagazine.com/*
  20. // @match *://distill.pub/*
  21. // @match *://github.com/*/*
  22. // @run-at document-idle
  23. // @grant GM_getResourceText
  24. // @grant GM_addStyle
  25. // @require https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.11.1/tocbot.min.js
  26. // @resource TOCBOT_STYLE https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.11.1/tocbot.css
  27. // @homepageURL https://github.com/hikerpig/toc-bar-userscript
  28. // ==/UserScript==
  29.  
  30. (function () {
  31. const SITE_SETTINGS = {
  32. jianshu: {
  33. contentSelector: '.ouvJEz',
  34. style: {
  35. top: '55px',
  36. color: '#ea6f5a',
  37. },
  38. },
  39. 'zhuanlan.zhihu.com': {
  40. contentSelector: '.Post-RichText',
  41. shouldShow() {
  42. return location.pathname.startsWith('/p/')
  43. },
  44. },
  45. zcfy: {
  46. contentSelector: '.markdown-body',
  47. },
  48. qq: {
  49. contentSelector: '.rich_media_content',
  50. },
  51. 'dev.to': {
  52. contentSelector: 'article',
  53. scrollSmoothOffset: -56,
  54. shouldShow() {
  55. return ['/search', '/top/'].every(s => !location.pathname.startsWith(s))
  56. },
  57. },
  58. 'medium.com': {
  59. contentSelector: 'article'
  60. },
  61. 'css-tricks.com': {
  62. contentSelector: 'main'
  63. },
  64. 'distill.pub': {
  65. contentSelector: 'body'
  66. },
  67. 'smashingmagazine': {
  68. contentSelector: 'article'
  69. },
  70. 'web.dev': {
  71. contentSelector: '#content'
  72. },
  73. 'github.com': {
  74. contentSelector() {
  75. const README_SEL = '#readme'
  76. const WIKI_CONTENT_SEL = '.repository-content'
  77. const matchedSel = [README_SEL, WIKI_CONTENT_SEL].find((sel) => {
  78. return !!document.querySelector(README_SEL)
  79. })
  80.  
  81. if (matchedSel) return matchedSel
  82.  
  83. return false
  84. }
  85. },
  86. }
  87.  
  88. function getSiteInfo() {
  89. let siteName
  90. if (SITE_SETTINGS[location.hostname]) {
  91. siteName = location.hostname
  92. } else {
  93. const match = location.href.match(
  94. /([\d\w]+)\.(com|cn|net|org|im|io|cc|site|tv)/i
  95. )
  96. siteName = match ? match[1] : null
  97. }
  98. if (siteName && SITE_SETTINGS[siteName]) {
  99. return {
  100. siteName,
  101. siteSetting: SITE_SETTINGS[siteName],
  102. }
  103. }
  104. }
  105.  
  106. function getPageTocOptions() {
  107. let siteInfo = getSiteInfo()
  108. if (siteInfo) {
  109. let siteSetting = siteInfo.siteSetting
  110. if (siteSetting.shouldShow && !siteSetting.shouldShow()) {
  111. return
  112. }
  113. if (typeof siteSetting.contentSelector === 'function') {
  114. const contentSelector = siteSetting.contentSelector()
  115. if (!contentSelector) return
  116. siteSetting = {...siteSetting, contentSelector}
  117. }
  118. console.log('[toc-bar] found site info for', siteInfo.siteName)
  119. return siteSetting
  120. }
  121. }
  122.  
  123. function loadStyles() {
  124. const tocbotCss = GM_getResourceText('TOCBOT_STYLE')
  125. if (tocbotCss) {
  126. GM_addStyle(tocbotCss)
  127. }
  128. }
  129.  
  130. /**
  131. * @param {String} content
  132. * @return {String}
  133. */
  134. function doContentHash(content) {
  135. const val = content.split('').reduce((prevHash, currVal) => (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
  136. return val.toString(32)
  137. }
  138.  
  139. // ---------------- TocBar ----------------------
  140. const TOC_BAR_STYLE = `
  141. .toc-bar {
  142. position: fixed;
  143. z-index: 9000;
  144. right: 5px;
  145. top: 80px;
  146. width: 340px;
  147. font-size: 14px;
  148. box-sizing: border-box;
  149. padding: 10px 10px 10px 0;
  150. box-shadow: 0 1px 3px #DDD;
  151. border-radius: 4px;
  152. transition: width 0.2s ease;
  153. color: #333;
  154. background: #FEFEFE;
  155. }
  156.  
  157. .toc-bar.toc-bar--collapsed {
  158. width: 30px;
  159. padding: 0;
  160. }
  161.  
  162. .toc-bar--collapsed .toc {
  163. display: none;
  164. }
  165.  
  166. .toc-bar--collapsed .toc-bar__toggle {
  167. transform: rotate(90deg);
  168. }
  169.  
  170. .toc-bar--collapsed .hidden-when-collapsed {
  171. display: none;
  172. }
  173.  
  174. .toc-bar__header {
  175. font-weight: bold;
  176. padding-bottom: 5px;
  177. display: flex;
  178. justify-content: space-between;
  179. align-items: center;
  180. cursor: move;
  181. }
  182.  
  183. .toc-bar__refresh {
  184. position: relative;
  185. top: -2px;
  186. }
  187.  
  188. .toc-bar__icon-btn {
  189. height: 1em;
  190. width: 1em;
  191. cursor: pointer;
  192. transition: transform 0.2s ease;
  193. }
  194.  
  195. .toc-bar__icon-btn:hover {
  196. opacity: 0.7;
  197. }
  198.  
  199. .toc-bar__icon-btn svg {
  200. max-width: 100%;
  201. max-height: 100%;
  202. }
  203.  
  204. .toc-bar__header-left {
  205. align-items: center;
  206. }
  207.  
  208. .toc-bar__toggle {
  209. cursor: pointer;
  210. padding: 2px 6px;
  211. box-sizing: content-box;
  212. transform: rotate(0);
  213. transition: transform 0.2s ease;
  214. }
  215.  
  216. .toc-bar__title {
  217. margin-left: 5px;
  218. }
  219.  
  220. .toc-bar a.toc-link {
  221. overflow: hidden;
  222. text-overflow: ellipsis;
  223. white-space: nowrap;
  224. display: block;
  225. line-height: 1.6;
  226. }
  227.  
  228. .flex {
  229. display: flex;
  230. }
  231.  
  232. /* override tocbot */
  233. .toc-bar .toc {
  234. max-height: 80vh;
  235. }
  236.  
  237. .toc>.toc-list li {
  238. padding-left: 8px;
  239. position: static;
  240. }
  241.  
  242. .toc-list-item > a:hover {
  243. text-decoration: underline;
  244. }
  245. /* end override tocbot */
  246. `
  247.  
  248. /**
  249. * @class
  250. */
  251. function TocBar() {
  252. // inject style
  253. GM_addStyle(TOC_BAR_STYLE)
  254.  
  255. this.element = document.createElement('div')
  256. this.element.id = 'toc-bar'
  257. this.element.classList.add('toc-bar')
  258. document.body.appendChild(this.element)
  259.  
  260. /** @type {Boolean} */
  261. this.visible = true
  262.  
  263. this.initHeader()
  264.  
  265. // create a container tocbot
  266. const tocElement = document.createElement('div')
  267. this.tocElement = tocElement
  268. tocElement.classList.add('toc')
  269. this.element.appendChild(tocElement)
  270. }
  271.  
  272. const TOC_ICON = `<svg t="1593614506959" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6049" width="200" height="200"><path d="M128 384h597.333333v-85.333333H128v85.333333z m0 170.666667h597.333333v-85.333334H128v85.333334z m0 170.666666h597.333333v-85.333333H128v85.333333z m682.666667 0h85.333333v-85.333333h-85.333333v85.333333z m0-426.666666v85.333333h85.333333v-85.333333h-85.333333z m0 256h85.333333v-85.333334h-85.333333v85.333334z" p-id="6050"></path></svg>`
  273. 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>`
  274.  
  275. TocBar.prototype = {
  276. /**
  277. * @method TocBar
  278. */
  279. initHeader() {
  280. const header = document.createElement('div')
  281. header.classList.add('toc-bar__header')
  282. header.innerHTML = `
  283. <div class="flex toc-bar__header-left">
  284. <div class="toc-bar__toggle toc-bar__icon-btn" title="Toggle TOC Bar">
  285. ${TOC_ICON}
  286. </div>
  287. <div class="toc-bar__title hidden-when-collapsed">TOC Bar</div>
  288. </div>
  289. <div class="toc-bar__actions hidden-when-collapsed">
  290. <div class="toc-bar__refresh toc-bar__icon-btn" title="Refresh TOC">
  291. ${REFRESH_ICON}
  292. </div>
  293. </div>
  294. `
  295. const toggleElement = header.querySelector('.toc-bar__toggle')
  296. toggleElement.addEventListener('click', () => {
  297. this.toggle()
  298. })
  299.  
  300. const refreshElement = header.querySelector('.toc-bar__refresh')
  301. refreshElement.addEventListener('click', () => {
  302. tocbot.refresh()
  303. })
  304. // ---------------- header drag ----------------------
  305. const dragState = {
  306. startMouseX: 0,
  307. startMouseY: 0,
  308. startPositionX: 0,
  309. startPositionY: 0,
  310. startElementDisToRight: 0,
  311. isDragging: false,
  312. }
  313.  
  314. const onMouseMove = (e) => {
  315. if (!dragState.isDragging) return
  316. const deltaX = e.pageX - dragState.startMouseX
  317. const deltaY = e.pageY - dragState.startMouseY
  318. // 要换算为 right 数字
  319. const newRight = dragState.startElementDisToRight - deltaX
  320. const newTop = dragState.startPositionY + deltaY
  321. // console.table({ newRight, newTop})
  322. this.element.style.right = `${newRight}px`
  323. this.element.style.top = `${newTop}px`
  324. }
  325.  
  326. const onMouseUp = (e) => {
  327. Object.assign(dragState, {
  328. isDragging: false,
  329. })
  330. document.body.removeEventListener('mousemove', onMouseMove)
  331. document.body.removeEventListener('mouseup', onMouseUp)
  332. }
  333.  
  334. header.addEventListener('mousedown', (e) => {
  335. if (e.target === toggleElement) return
  336. const bbox = this.element.getBoundingClientRect()
  337. Object.assign(dragState, {
  338. isDragging: true,
  339. startMouseX: e.pageX,
  340. startMouseY: e.pageY,
  341. startPositionX: bbox.x,
  342. startPositionY: bbox.y,
  343. startElementDisToRight: document.body.clientWidth - bbox.right,
  344. })
  345. document.body.addEventListener('mousemove', onMouseMove)
  346. document.body.addEventListener('mouseup', onMouseUp)
  347. })
  348. // ----------------end header drag -------------------
  349.  
  350. this.element.appendChild(header)
  351. },
  352. /**
  353. * @method TocBar
  354. */
  355. initTocbot(options) {
  356. const me = this
  357. const tocbotOptions = Object.assign(
  358. {},
  359. {
  360. tocSelector: '.toc',
  361. scrollSmoothOffset: options.scrollSmoothOffset || 0,
  362. // hasInnerContainers: true,
  363. headingObjectCallback(obj, ele) {
  364. // if there is no id on the header element, add one that derived from hash of header title
  365. if (!ele.id) {
  366. const newId = me.generateHeaderId(obj, ele)
  367. ele.setAttribute('id', newId)
  368. obj.id = newId
  369. }
  370. return obj
  371. },
  372. headingSelector: 'h1, h2, h3, h4, h5',
  373. collapseDepth: 4,
  374. },
  375. options
  376. )
  377. // console.log('tocbotOptions', tocbotOptions);
  378. tocbot.init(tocbotOptions)
  379. },
  380. generateHeaderId(obj, ele) {
  381. return `tocbar-${doContentHash(obj.textContent)}`
  382. },
  383. /**
  384. * @method TocBar
  385. */
  386. toggle(shouldShow = !this.visible) {
  387. const HIDDEN_CLASS = 'toc-bar--collapsed'
  388. if (shouldShow) {
  389. this.element.classList.remove(HIDDEN_CLASS)
  390. } else {
  391. this.element.classList.add(HIDDEN_CLASS)
  392. }
  393. this.visible = shouldShow
  394. },
  395. }
  396. // ----------------end TocBar -------------------
  397.  
  398. function main() {
  399. const options = getPageTocOptions()
  400. if (options) {
  401. loadStyles()
  402.  
  403. const tocBar = new TocBar()
  404. tocBar.initTocbot(options)
  405. }
  406. }
  407.  
  408. main()
  409. })()