TreeBar

目录树导航 - 显示文章目录大纲导航

  1. // ==UserScript==
  2. // @name TreeBar
  3. // @name:zh-CN 目录树导航
  4. // @namespace https://github.com/zhilidali/TreeBar/
  5. // @version 0.1.11
  6. // @description 目录树导航 - 显示文章目录大纲导航
  7. // @description:zh-cn 目录树导航 - 显示文章目录大纲导航
  8. // @author zhilidali
  9. // @mail zhilidali@qq.com
  10. // @license MIT Licensed
  11. // @match *://www.jianshu.com/p/*
  12. // @match *://cdn2.jianshu.io/p/*
  13. // @match *://juejin.im/post/*
  14. // @match *://juejin.im/entry/*
  15. // @match *://sspai.com/*
  16. // @match *://zhuanlan.zhihu.com/p/*
  17. // @match *://mp.weixin.qq.com/s?*
  18. // @match *://cnodejs.org/topic/*
  19. // @match *://div.io/topic/*
  20. // @match *://www.zcfy.cc/article/*
  21. // @grant none
  22. // ==/UserScript==
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. var map = {
  28. jianshu: {
  29. tagName: '.ouvJEz',
  30. style: {
  31. top: '55px',
  32. color: '#ea6f5a',
  33. }
  34. },
  35. zhihu: {
  36. tagName: '.Post-RichText',
  37. },
  38. sspai: {
  39. tagName: '.notion-page-content',
  40. },
  41. juejin: {
  42. tagName: '.article-content',
  43. },
  44. div: {
  45. tagName: '.topic-firstfloor-detail'
  46. },
  47. zcfy: {
  48. tagName: '.markdown-body',
  49. },
  50. qq: {
  51. tagName: '.rich_media_content',
  52. },
  53. default: {
  54. tagName: 'body'
  55. }
  56. };
  57. var treeBar = window.treeBar = {
  58. site: {
  59. name: '',
  60. tagName: '',
  61. },
  62. className: 'treeBar',
  63. style: `
  64. /* 样式重置 */
  65. html { scroll-behavior: smooth; }
  66. .treeBar ul { padding-left: 1.1em; margin: 0; }
  67. .treeBar > ul > li { list-style-type: disc; }
  68. .treeBar > ul > li > ul > li { list-style-type: circle; }
  69. .treeBar > ul > li > ul > li > ul > li { list-style-type: square; }
  70. /* common */
  71. .treeBar {
  72. z-index: 99999; position: fixed; top: 10%; right: 0; max-width: 300px; max-height: 88%;
  73. border: 1px solid #ddd; overflow-y: auto; overflow-x: visible; background-color: rgba(255, 255, 255, .9);
  74. }
  75. .treeBar-resize {
  76. position: absolute; /*cursor: col-resize;*/ width: 5px; left: -2px; top: 0; bottom: 0;
  77. }
  78. .treeBar-btn {
  79. box-sizing: border-box; position: absolute; top: -1px; left: -1px; width: 72px; height: 28px;
  80. padding: 0; border: 1px solid #ddd; border-radius: 3px; box-shadow: 0 1px 1px 1px #ddd;
  81. font-size: 14px; background-color: #fff; vertical-align: middle; text-align: center;
  82. outline: none; cursor: pointer; color: #333;
  83. }
  84. .treeBar > ul {
  85. padding: 30px 10px 10px 25px;
  86. }
  87. .treeBar > ul > li a {
  88. line-height: 30px; /*overflow: hidden; white-space: nowrap; text-overflow: ellipsis;*/
  89. text-decoration: none; font-size: 14px; cursor: pointer; color: #0371e9;
  90. }
  91. .treeBar > ul > li a:hover {
  92. text-decoration: underline;
  93. }
  94. /* slideToggle */
  95. .treeBar-slide { overflow-y: visible; }
  96. .treeBar-slide .treeBar-btn { left: -71px; top: -1px; }
  97. .treeBar-slide > ul { display: none; }`,
  98. innerDom: `<button class="treeBar-btn">TreeBar</button><div class="treeBar-resize"></div>`,
  99. matchSite: function() {/* 匹配站点 */
  100. var domain = location.href.match(/([\d\w]+)\.(com|cn|net|org|im|io|cc)/i);
  101. this.site.name = (domain && domain[1]);
  102. var siteInfo = map[this.site.name] || map.default;
  103. this.site.tagName = siteInfo.tagName;
  104. this.site.sync = siteInfo.sync || false;
  105. if (siteInfo.style) {
  106. this.style += `
  107. .treeBar {
  108. top: ${siteInfo.style.top};
  109. border-color: ${siteInfo.style.color};
  110. }
  111. .treeBar-btn {
  112. border-color: ${siteInfo.style.color};
  113. background-color: ${siteInfo.style.color};
  114. color: #fff;
  115. }
  116. .treeBar > ul > li a {
  117. color: ${siteInfo.style.color};
  118. }
  119. `;
  120. }
  121. },
  122. createDom: function() {/* 创建DOM */
  123. var style = document.createElement('style'),
  124. dom = document.createElement('div');
  125. style.innerHTML = this.style;
  126. document.head.appendChild(style);
  127. dom.className = this.className;
  128. dom.innerHTML = this.innerDom + this.ul;
  129. document.body.appendChild(dom);
  130. },
  131. onEvent: function() {
  132. var eTree = document.querySelector('.treeBar'),
  133. eBtn = document.querySelector('.treeBar-btn'),
  134. eResize = document.querySelector('.treeBar-resize');
  135. eBtn.onclick = function () {
  136. eTree.classList.toggle('treeBar-slide');
  137. };
  138. /*var resize = {};
  139. document.body.onmouseup = function() {
  140. console.log('up');
  141. resize.flag = false;
  142. };
  143. eResize.onmousedown = function() {
  144. resize.flag = true;
  145. };
  146. document.body.onmousemove = function() {
  147. if (!resize.flag) return;
  148. if (resize.current === undefined) {
  149. resize.current = event.x;
  150. return;
  151. }
  152. var x = event.x - resize.current;
  153. if(Math.abs(x) >= 10) {
  154. tree.style.width = tree.offsetWidth - x + 'px';
  155. console.log(x);
  156. resize.current = event.x;
  157. }
  158. };*/
  159. },
  160. init: function() {
  161. console.time('TreeBar');
  162.  
  163. // 1. 获取DOM
  164. var hList = document.querySelector(treeBar.site.tagName)
  165. .querySelectorAll('h1, h2, h3, h4, h5, h6');
  166. // 2. 构建数据
  167. var tree = transformTree(Array.from(hList));
  168. // 3. 构建DOM
  169. treeBar.ul = compileList(tree);
  170. treeBar.createDom();
  171. // 4. 注册事件
  172. treeBar.onEvent();
  173.  
  174. console.timeEnd('TreeBar');
  175. }
  176. };
  177.  
  178. // 0. 匹配站点
  179. treeBar.matchSite();
  180. if (treeBar.site.tagName !== 'body' && document.querySelector(treeBar.site.tagName)) {
  181. treeBar.init();
  182. } else {
  183. document.onreadystatechange = function () {
  184. document.readyState === "complete" && treeBar.init();
  185. };
  186. }
  187.  
  188.  
  189. /* 解析DOM,构建树形数据 */
  190. function transformTree(list) {
  191. var result = [];
  192. list.reduce(function (res, cur, index, arr) {
  193. var prev = res[res.length - 1];
  194. if (compare(prev, cur)) {
  195. if (!prev.sub) prev.sub = [];
  196. prev.sub.push(cur);
  197. if (index === arr.length - 1) prev.sub = transformTree(prev.sub);
  198. } else {
  199. construct(res, cur);
  200. if (prev && prev.sub) prev.sub = transformTree(prev.sub);
  201. }
  202. return res;
  203. }, result);
  204.  
  205. // 转为树形结构的条件依据
  206. function compare(prev, cur) {
  207. return prev && cur.tagName.replace(/h/i, '') > prev.tagName.replace(/h/i, '');
  208. }
  209.  
  210. // 转为树形结构后的数据改造
  211. function construct(arr, obj) {
  212. arr.push({
  213. name: obj.innerText,
  214. id: obj.id = obj.innerText,
  215. tagName: obj.tagName
  216. });
  217. }
  218.  
  219. return result;
  220. }
  221.  
  222. /* 根据数据构建目录 */
  223. function compileList(tree) {
  224. var list = '';
  225. tree.forEach(function(item) {
  226. var ul = item.sub ? compileList(item.sub) : '';
  227. list +=
  228. `<li>
  229. <a href="#${item.id}" title="${item.name}">${item.name}</a>${ul}
  230. </li>`;
  231. });
  232. return `<ul>${list}</ul>`;
  233. }
  234.  
  235. })();