🏠 Home 

Github, code cloud md file directory

Github,code cloud project README.md add directory sidebar navigation,Floating button

  1. // ==UserScript==
  2. // @name github、码云 md文件目录化
  3. // @name:en Github, code cloud md file directory
  4. // @namespace github、码云 md文件目录化
  5. // @version 1.14
  6. // @description github、码云、npmjs项目README.md增加目录侧栏导航,悬浮按钮
  7. // @description:en Github,code cloud project README.md add directory sidebar navigation,Floating button
  8. // @author lecoler
  9. // @supportURL https://github.com/lecoler/md-list
  10. // @icon https://raw.githubusercontent.com/lecoler/readme.md-list/master/static/icon.png
  11. // @match *://gitee.com/*/*
  12. // @match *://www.gitee.com/*/*
  13. // @match *://github.com/*/*
  14. // @match *://www.github.com/*/*
  15. // @match *://npmjs.com/*/*
  16. // @match *://www.npmjs.com/*/*
  17. // @include *.md
  18. // @note 2022.03.18-v1.14 降低悬浮球位置,修改样式
  19. // @note 2021.01.09-v1.13 修复高亮bug
  20. // @note 2021.01.09-v1.12 新增根据页面阅读进度高亮
  21. // @note 2020.11.10-v1.11 修复标题显示标签化问题
  22. // @note 2020.10.30-v1.10 Fix not find node
  23. // @note 2020.09.15-V1.9 优化,移除计时器,改成用户触发加载检测,同时为检测失败添加‘移除目录’按钮(测试版)
  24. // @note 2020.09.14-V1.8 新增支持全部网站 *.md(测试版)
  25. // @note 2020.07.14-V1.7 新增当前页面有能解析的md才展示
  26. // @note 2020.06.23-V1.6 css样式进行兼容处理
  27. // @note 2020.05.22-V1.5 新增支持github wiki 页
  28. // @note 2020.05.20-V1.4 拖动按钮坐标改用百分比,对窗口大小改变做相应适配
  29. // @note 2020.02.10-V1.3 修改样式,整个按钮可点;新增支持 npmjs.com
  30. // @note 2019.12.04-V1.2 新增容错
  31. // @note 2019.10.31-V1.1 修改样式,新增鼠标右键返回顶部
  32. // @note 2019.10.28-V1.0 优化逻辑,追加判断目录内容是否存在
  33. // @note 2019.10.25-V0.9 重构项目,移除jq,改用原生开发,新增悬浮按钮
  34. // @note 2019.10.14-V0.9 修复bug
  35. // @note 2019.9.18-V0.8 修改样式,新增可手动拉伸
  36. // @note 2019.9.11-V0.7 新增点击跳转前判断是否能跳,不能将回到主页执行跳转
  37. // @note 2019.8.11-V0.6 优化代码,修改样式
  38. // @note 2019.7.25-V0.5 美化界面
  39. // @note 2019.7.25-V0.4 新增支持github
  40. // @note 2019.7.25-V0.2 修复bug,优化运行速度,新增按序获取
  41. // @home-url https://greasyfork.org/zh-CN/scripts/387834
  42. // @homepageURL https://github.com/lecoler/md-list
  43. // @run-at document-end
  44. // ==/UserScript==
  45. (function () {
  46. 'use strict';
  47. // 初始化
  48. let reload = false; // 是否需重载
  49. let $main = null;
  50. let $menu = null;
  51. let $button = null;
  52. let lastPathName = '';
  53. let moveStatus = false;
  54. let titleHeight = 0;
  55. // 初始化按钮
  56. function createDom() {
  57. // 往页面插入样式表
  58. style();
  59. // 创建主容器
  60. $main = document.createElement('div');
  61. // 创建按钮
  62. $button = document.createElement('div');
  63. // 创建菜单
  64. $menu = document.createElement('ul');
  65. // 按钮设置
  66. $button.innerHTML = `目录`;
  67. $button.title = '右键返回顶部(RM to Top)';
  68. // 添加点击事件
  69. $button.addEventListener('click', btnClick);
  70. // 添加右键点击事件
  71. $button.oncontextmenu = e => {
  72. // 回到顶部
  73. scrollTo(0, 0);
  74. return false;
  75. };
  76. // 往主容器添加dom
  77. $main.appendChild($button);
  78. $main.appendChild($menu);
  79. // 主容器设置样式
  80. $main.setAttribute('class', 'le-md');
  81. // 为按钮添加拖动
  82. dragEle($button);
  83. // 往页面添加主容器
  84. document.body.appendChild($main);
  85. // 监听窗口大小
  86. window.onresize = function () {
  87. // 隐藏列表
  88. if (!$menu.className.match(/hidden/)) {
  89. $menu.className += ' hidden';
  90. }
  91. };
  92. }
  93. // 按钮点击事件
  94. function btnClick(e) {
  95. //判断是否在移动
  96. if (moveStatus) {
  97. moveStatus = false;
  98. return false;
  99. }
  100. if ($menu.className.match(/hidden/)) {
  101. // 判断路径是否改变,menu是否重载
  102. if (lastPathName !== window.location.pathname || reload) {
  103. start(true);
  104. }
  105. // 判断menu位置
  106. const winWidth = document.documentElement.clientWidth;
  107. const winHeight = document.documentElement.clientHeight;
  108. const x = e.clientX;
  109. const y = e.clientY;
  110. const classname1 = winWidth / 2 - x > 0 ? 'le-md-right' : 'le-md-left';
  111. const classname2 = winHeight / 2 - y > 0 ? 'le-md-bottom' : 'le-md-top';
  112. $menu.className = `${classname1} ${classname2}`;
  113. } else {
  114. $menu.className += ' hidden';
  115. }
  116. }
  117. // 插入样式表
  118. function style() {
  119. const style = document.createElement('style');
  120. style.innerHTML = `
  121. .le-md {
  122. position: fixed;
  123. top: 16%;
  124. left: 90%;
  125. z-index: 999;
  126. }
  127. .le-md-btn {
  128. display: block;
  129. font-size: 14px;
  130. text-transform: uppercase;
  131. width: 60px;
  132. height: 60px;
  133. -webkit-box-sizing: border-box;
  134. box-sizing: border-box;
  135. border-radius: 50%;
  136. color: #fff;
  137. text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.8);
  138. border: 0;
  139. background: hsla(230, 50%, 50%, 0.6);
  140. -webkit-animation: pulse 1s infinite alternate;
  141. animation: pulse 1s infinite alternate;
  142. -webkit-transition: background 0.4s, margin 0.2s;
  143. -o-transition: background 0.4s, margin 0.2s;
  144. transition: background 0.4s, margin 0.2s;
  145. text-align: center;
  146. line-height: 60px;
  147. -webkit-user-select: none;
  148. -moz-user-select: none;
  149. -ms-user-select: none;
  150. user-select: none;
  151. cursor: move;
  152. }
  153. .le-md-btn:after {
  154. background: rgba(0, 0, 0, 0.05);
  155. border-radius: 50%;
  156. bottom: -22.5px;
  157. content: "";
  158. display: block;
  159. height: 0;
  160. margin: 0 auto;
  161. left: 0;
  162. position: absolute;
  163. right: 0;
  164. width: 40px;
  165. -webkit-transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
  166. -o-transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
  167. transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
  168. -webkit-animation: shadow 1s infinite alternate;
  169. animation: shadow 1s infinite alternate;
  170. }
  171. .le-md-btn:hover {
  172. background: hsla(220, 50%, 47%, 1);
  173. margin-top: -1px;
  174. -webkit-animation: none;
  175. animation: none;
  176. -webkit-box-shadow: inset -5px -10px 1px hsla(220, 50%, 42%, 1);
  177. box-shadow: inset -5px -10px 1px hsla(220, 50%, 42%, 1);
  178. }
  179. .le-md-btn:hover:after {
  180. -webkit-animation: none;
  181. animation: none;
  182. height: 10px;
  183. }
  184. .le-md-btn-hidden{
  185. display: none;
  186. -webkit-animation: none;
  187. animation: none;
  188. }
  189. .hidden {
  190. height: 0 !important;
  191. min-height: 0 !important;
  192. border: 0 !important;
  193. }
  194. .le-md-left {
  195. right: 0;
  196. margin-right: 100px;
  197. }
  198. .le-md-right {
  199. left: 0;
  200. margin-left: 100px;
  201. }
  202. .le-md-top {
  203. bottom: 0;
  204. }
  205. .le-md-bottom {
  206. top: 0;
  207. }
  208. .le-md > ul {
  209. width: 200px;
  210. min-width: 100px;
  211. max-width: 1000px;
  212. list-style: none;
  213. position: absolute;
  214. overflow: auto;
  215. -webkit-transition: min-height 0.4s;
  216. -o-transition: min-height 0.4s;
  217. transition: min-height 0.4s;
  218. min-height: 50px;
  219. height: auto;
  220. max-height: 700px;
  221. resize: both;
  222. padding-right: 10px;
  223. }
  224. .le-md > ul::-webkit-scrollbar {
  225. width: 8px;
  226. height: 1px;
  227. }
  228. .le-md > ul::-webkit-scrollbar-thumb {
  229. border-radius: 8px;
  230. -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
  231. box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
  232. background-color: #96C2F1;
  233. background-image: linear-gradient(
  234. 45deg,
  235. rgba(255, 255, 255, 0.2) 25%,
  236. transparent 25%,
  237. transparent 50%,
  238. rgba(255, 255, 255, 0.2) 50%,
  239. rgba(255, 255, 255, 0.2) 75%,
  240. transparent 75%,
  241. transparent
  242. );
  243. }
  244. .le-md > ul::-webkit-scrollbar-track {
  245. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
  246. box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
  247. border-radius: 8px 8px 0 0;
  248. background: #EFF7FF;
  249. }
  250. .le-md > ul a:hover {
  251. background: #fff;
  252. border-left: 1em groove #0099CC !important;
  253. }
  254. .le-md > ul a {
  255. text-decoration: none;
  256. font-size: 1em;
  257. color: #909399;
  258. text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
  259. display: block;
  260. white-space: nowrap;
  261. -o-text-overflow: ellipsis;
  262. text-overflow: ellipsis;
  263. overflow: hidden;
  264. padding: 5px 10px;
  265. border-bottom: 0.5em solid #eee;
  266. -webkit-transition: 0.4s all;
  267. -o-transition: 0.4s all;
  268. transition: 0.4s all;
  269. border-left: 0.5em groove #e2e2e2;
  270. border-right: 1px solid #e2e2e2;
  271. border-top: 1px solid #e2e2e2;
  272. background: #f4f4f5;
  273. -webkit-box-sizing: border-box;
  274. box-sizing: border-box;
  275. -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  276. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  277. border-radius: 0 0 5px 5px;
  278. }
  279. @-webkit-keyframes pulse {
  280. 0% {
  281. margin-top: 0;
  282. }
  283. 100% {
  284. margin-top: 6px;
  285. -webkit-box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
  286. box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
  287. }
  288. }
  289. @keyframes pulse {
  290. 0% {
  291. margin-top: 0;
  292. }
  293. 100% {
  294. margin-top: 6px;
  295. -webkit-box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
  296. box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
  297. }
  298. }
  299. @-webkit-keyframes shadow {
  300. to {
  301. height: 16px;
  302. }
  303. }
  304. @keyframes shadow {
  305. to {
  306. height: 16px;
  307. }
  308. }
  309. .le-md li.le-md-title-active a{
  310. background: linear-gradient(-135deg, #ffcccc 0.6em, #fff 0);
  311. }
  312. .le-md li.le-md-title-active.le-md-title-active-first a{
  313. background: linear-gradient(-135deg, #ff9999 0.6em, #fff 0);
  314. color: #000;
  315. font-weight: 700;
  316. }
  317. `;
  318. document.head.appendChild(style);
  319. }
  320. // 拖动事件
  321. function dragEle(ele) {
  322. ele.onmousedown = event => {
  323. // 鼠标相对dom坐标
  324. let eleX = event.offsetX;
  325. let eleY = event.offsetY;
  326. let count = 0;
  327. window.document.onmousemove = e => {
  328. //防止误触移动
  329. if (count > 9) {
  330. moveStatus = true;
  331. }
  332. // dom相对win坐标
  333. let winX = e.clientX;
  334. let winY = e.clientY;
  335. // 实际坐标
  336. let x = winX - eleX;
  337. let y = winY - eleY;
  338. // win长宽
  339. let winWidth = document.documentElement.clientWidth;
  340. let winHeight = document.documentElement.clientHeight;
  341. // 转化成百分比
  342. ele.parentNode.style.left = (x / winWidth).toFixed(3) * 100 + '%';
  343. ele.parentNode.style.top = (y / winHeight).toFixed(3) * 100 + '%';
  344. count++;
  345. };
  346. };
  347. ele.onmouseup = () => {
  348. window.document.onmousemove = null;
  349. };
  350. ele.onmouseout = () => {
  351. window.document.onmousemove = null;
  352. };
  353. }
  354. // 执行, flag 是否部分重载
  355. function start(flag) {
  356. // 初始化
  357. reload = false;
  358. // 获取链接
  359. const host = window.location.host;
  360. lastPathName = window.location.pathname;
  361. // 获取相应的容器dom
  362. let $content = null;
  363. let list = [];
  364. if (host === 'github.com') {
  365. //github home / wiki
  366. const $parent = document.getElementById('readme') || document.getElementById('wiki-body');
  367. $content = $parent && $parent.getElementsByClassName('markdown-body')[0];
  368. // 标题dom高度
  369. const $boxTitle = ($parent && $parent.parentElement) ? $parent.parentElement.getElementsByClassName('js-sticky')[0] : null;
  370. titleHeight = $boxTitle ? $boxTitle.offsetHeight + 2 : 0;
  371. // 监听github dom的变化
  372. // !$menu && domChangeListener(document.getElementById('js-repo-pjax-container'), start);
  373. !$menu && window.addEventListener('pjax:complete', start);
  374. } else if (host === 'gitee.com') {
  375. //码云 home
  376. const $parent = document.getElementById('tree-content-holder');
  377. $content = $parent && $parent.getElementsByClassName('markdown-body')[0];
  378. // 监听gitee dom的变化
  379. !$menu && domChangeListener(document.getElementById('tree-holder'), start);
  380. } else if (host === 'www.npmjs.com') {
  381. // npmjs.com
  382. const $parent = document.getElementById('readme');
  383. $content = $parent ? $parent : null;
  384. } else {
  385. // 检测是否符合md格式
  386. $content = checkMd();
  387. }
  388. // 获取子级
  389. const $children = $content ? $content.children : [];
  390. for (let $dom of $children) {
  391. const tagName = $dom.tagName;
  392. const lastCharAt = +tagName.charAt(tagName.length - 1);
  393. // 获取Tag h0-h9
  394. if (tagName.length === 2 && tagName.startsWith('H') && !isNaN(lastCharAt)) {
  395. // 获取value
  396. const value = $dom.innerText.trim();
  397. // 新增容错率
  398. const $a = $dom.getElementsByTagName('a')[0];
  399. if ($a) {
  400. // 获取锚点
  401. const href = $a.getAttribute('href');
  402. // 获取offsetTop
  403. const offsetTop = getTop($a)
  404. list.push({
  405. type: lastCharAt,
  406. value,
  407. href,
  408. offsetTop
  409. });
  410. }
  411. }
  412. }
  413. // 清空容器,不存在则创建
  414. if ($menu) {
  415. const list = [...$menu.childNodes];
  416. list.forEach(i => $menu.removeChild(i));
  417. } else {
  418. createDom();
  419. }
  420. if (!$menu || !$button) {
  421. console.warn('md文件目录化 脚本初始化失败');
  422. return false;
  423. }
  424. // 隐藏菜单
  425. if (!flag) {
  426. $menu.className = 'hidden';
  427. }
  428. //是否存在
  429. if (list.length) {
  430. // 生成菜单
  431. for (let i of list) {
  432. const li = document.createElement('li');
  433. li.setAttribute('data-offsetTop', i.offsetTop)
  434. const a = document.createElement('a');
  435. a.href = i.href;
  436. a.title = i.value;
  437. a.style = `font-size: ${1.3 - i.type * 0.1}em;margin-left: ${i.type - 1}em;border-left: 0.5em groove hsla(200, 80%, ${45 + i.type * 10}%, 0.8);`;
  438. a.innerText = i.value;
  439. li.appendChild(a);
  440. $menu.appendChild(li);
  441. // 是否不符合规范
  442. if (!i.value) {
  443. reload = true;
  444. }
  445. }
  446. // 提供关闭入口
  447. if (reload) {
  448. const li = document.createElement('li');
  449. li.innerHTML = `<a title="移除目录" style="font-size: 1.1em;margin-left: 0.1em;border-left: 0.5em groove hsla(0,80%,50%,0.8);">移除目录</a>`;
  450. // 添加事件
  451. li.onclick = function () {
  452. $main.remove();
  453. };
  454. $menu.appendChild(li);
  455. }
  456. // 设置按钮样式
  457. $button.setAttribute('class', 'le-md-btn');
  458. } else {
  459. // 设置按钮样式
  460. $button.setAttribute('class', 'le-md-btn le-md-btn-hidden');
  461. }
  462. }
  463. /**
  464. * @Description 监听指定dom发现变化事件
  465. * @author lecoler
  466. * @date 2020/7/14
  467. * @param dom
  468. * @param fun 回调 (MutationRecord[],MutationObserver)
  469. * @param opt 额外参数
  470. * @return MutationObserver
  471. */
  472. function domChangeListener(dom, fun, opt = {}) {
  473. if (!dom) return null;
  474. const observe = new MutationObserver(fun);
  475. observe.observe(dom, Object.assign({
  476. childList: true,
  477. attributes: true,
  478. }, opt));
  479. return observe;
  480. }
  481. /**
  482. * @Description 判断是否符合格式的md
  483. * @author lecoler
  484. * @date 2020/9/14
  485. * @return DOM
  486. */
  487. function checkMd() {
  488. // 缓存
  489. let tmp = [];
  490. // 是否存在h1 h2 h3 h4 h5 ...标签,同时他们父级相同
  491. for (let i = 1; i < 7; i++) {
  492. let list = document.body.getElementsByTagName(`h${i}`);
  493. // 获取父级
  494. for (let i = 0; i < list.length; i++) {
  495. const parent = list[i].parentElement;
  496. const item = tmp.filter(j => j && j['ele'].isEqualNode(parent))[0];
  497. if (item) {
  498. item.count += 1;
  499. } else {
  500. tmp.push({
  501. ele: parent,
  502. count: 1,
  503. });
  504. }
  505. }
  506. }
  507. // 排序
  508. tmp.sort((a, b) => b.count - a.count);
  509. // 获取出现次数最高父级 返回
  510. return tmp.length ? tmp[0]['ele'] : null;
  511. }
  512. // 监听Windows滚动事件
  513. function onScrollEvent() {
  514. const fun = debounce(updateTitleActive, 500)
  515. // 判断原页面是否存在滚动事件监听,存在则合并,否则新建
  516. const oldFun = window.onscroll
  517. // 存在
  518. if (oldFun && oldFun.constructor === Function) {
  519. window.onscroll = function () {
  520. // 触发原页面事件
  521. oldFun.call(this)
  522. // 刷新标题 active 状态
  523. fun()
  524. }
  525. } else {
  526. window.onscroll = function () {
  527. // 刷新标题 active 状态
  528. fun()
  529. }
  530. }
  531. }
  532. /**
  533. * @Description 防抖动
  534. * @author lecoler
  535. * @date 2020/7/1
  536. * @param func<Function>
  537. * @param time<number>
  538. * @return Function
  539. */
  540. function debounce(func, time) {
  541. let context, args, timeId, timestamp
  542. function timeout() {
  543. const now = Date.now() - timestamp
  544. if (now >= 0 && now < time) {
  545. timeId = setTimeout(timeout, time - now)
  546. } else {
  547. timeId = null
  548. func.apply(context, args)
  549. }
  550. }
  551. function action() {
  552. context = this
  553. args = arguments
  554. timestamp = Date.now()
  555. if (!timeId) timeId = setTimeout(timeout, time)
  556. }
  557. return action
  558. }
  559. // 更新标题active状态
  560. function updateTitleActive() {
  561. // 获取目前页面scrollTop
  562. const ScrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0
  563. const scrollTop = ScrollTop + titleHeight
  564. const offsetHeight = document.documentElement.clientHeight || document.body.clientHeight || 0
  565. const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight || 0
  566. // 存在菜单
  567. if ($menu) {
  568. const list = $menu.children || []
  569. for (let i = 0; i < list.length; i++) {
  570. const val = list[i].getAttribute('data-offsetTop')
  571. const nextVal = list[i + 1] ? list[i + 1].getAttribute('data-offsetTop') : scrollHeight
  572. // 排他
  573. list[i].removeAttribute('class')
  574. // 肉眼可见部分,标题高亮
  575. if (scrollTop <= val && val <= offsetHeight + scrollTop) {
  576. list[i].className = 'le-md-title-active'
  577. }
  578. // 正在阅读部分,标题高亮
  579. if (scrollTop >= val && nextVal > scrollTop) {
  580. list[i].className = 'le-md-title-active le-md-title-active-first'
  581. }
  582. }
  583. }
  584. }
  585. /**
  586. * @describe 获取dom元素距离body的offsetTop
  587. * @author lecoler
  588. * @date 21-1-8
  589. * @param $dom<Node>
  590. * @return Number
  591. */
  592. function getTop($dom, val = 0) {
  593. if (!$dom) return val
  594. const offsetTop = $dom.offsetTop || 0
  595. return getTop($dom.offsetParent, offsetTop + val)
  596. }
  597. try {
  598. document.onreadystatechange = function () {
  599. if (document.readyState === 'complete') {
  600. start();
  601. // 监听滚动
  602. onScrollEvent()
  603. }
  604. };
  605. } catch (e) {
  606. console.error('github、码云 md文件目录化 脚本异常报错:');
  607. console.error(e);
  608. console.error('请联系作者修复解决,https://github.com/lecoler/md-list');
  609. }
  610. })();