Github,code cloud project README.md add directory sidebar navigation,Floating button
- // ==UserScript==
- // @name github、码云 md文件目录化
- // @name:en Github, code cloud md file directory
- // @namespace github、码云 md文件目录化
- // @version 1.14
- // @description github、码云、npmjs项目README.md增加目录侧栏导航,悬浮按钮
- // @description:en Github,code cloud project README.md add directory sidebar navigation,Floating button
- // @author lecoler
- // @supportURL https://github.com/lecoler/md-list
- // @icon https://raw.githubusercontent.com/lecoler/readme.md-list/master/static/icon.png
- // @match *://gitee.com/*/*
- // @match *://www.gitee.com/*/*
- // @match *://github.com/*/*
- // @match *://www.github.com/*/*
- // @match *://npmjs.com/*/*
- // @match *://www.npmjs.com/*/*
- // @include *.md
- // @note 2022.03.18-v1.14 降低悬浮球位置,修改样式
- // @note 2021.01.09-v1.13 修复高亮bug
- // @note 2021.01.09-v1.12 新增根据页面阅读进度高亮
- // @note 2020.11.10-v1.11 修复标题显示标签化问题
- // @note 2020.10.30-v1.10 Fix not find node
- // @note 2020.09.15-V1.9 优化,移除计时器,改成用户触发加载检测,同时为检测失败添加‘移除目录’按钮(测试版)
- // @note 2020.09.14-V1.8 新增支持全部网站 *.md(测试版)
- // @note 2020.07.14-V1.7 新增当前页面有能解析的md才展示
- // @note 2020.06.23-V1.6 css样式进行兼容处理
- // @note 2020.05.22-V1.5 新增支持github wiki 页
- // @note 2020.05.20-V1.4 拖动按钮坐标改用百分比,对窗口大小改变做相应适配
- // @note 2020.02.10-V1.3 修改样式,整个按钮可点;新增支持 npmjs.com
- // @note 2019.12.04-V1.2 新增容错
- // @note 2019.10.31-V1.1 修改样式,新增鼠标右键返回顶部
- // @note 2019.10.28-V1.0 优化逻辑,追加判断目录内容是否存在
- // @note 2019.10.25-V0.9 重构项目,移除jq,改用原生开发,新增悬浮按钮
- // @note 2019.10.14-V0.9 修复bug
- // @note 2019.9.18-V0.8 修改样式,新增可手动拉伸
- // @note 2019.9.11-V0.7 新增点击跳转前判断是否能跳,不能将回到主页执行跳转
- // @note 2019.8.11-V0.6 优化代码,修改样式
- // @note 2019.7.25-V0.5 美化界面
- // @note 2019.7.25-V0.4 新增支持github
- // @note 2019.7.25-V0.2 修复bug,优化运行速度,新增按序获取
- // @home-url https://greasyfork.org/zh-CN/scripts/387834
- // @homepageURL https://github.com/lecoler/md-list
- // @run-at document-end
- // ==/UserScript==
- (function () {
- 'use strict';
- // 初始化
- let reload = false; // 是否需重载
- let $main = null;
- let $menu = null;
- let $button = null;
- let lastPathName = '';
- let moveStatus = false;
- let titleHeight = 0;
- // 初始化按钮
- function createDom() {
- // 往页面插入样式表
- style();
- // 创建主容器
- $main = document.createElement('div');
- // 创建按钮
- $button = document.createElement('div');
- // 创建菜单
- $menu = document.createElement('ul');
- // 按钮设置
- $button.innerHTML = `目录`;
- $button.title = '右键返回顶部(RM to Top)';
- // 添加点击事件
- $button.addEventListener('click', btnClick);
- // 添加右键点击事件
- $button.oncontextmenu = e => {
- // 回到顶部
- scrollTo(0, 0);
- return false;
- };
- // 往主容器添加dom
- $main.appendChild($button);
- $main.appendChild($menu);
- // 主容器设置样式
- $main.setAttribute('class', 'le-md');
- // 为按钮添加拖动
- dragEle($button);
- // 往页面添加主容器
- document.body.appendChild($main);
- // 监听窗口大小
- window.onresize = function () {
- // 隐藏列表
- if (!$menu.className.match(/hidden/)) {
- $menu.className += ' hidden';
- }
- };
- }
- // 按钮点击事件
- function btnClick(e) {
- //判断是否在移动
- if (moveStatus) {
- moveStatus = false;
- return false;
- }
- if ($menu.className.match(/hidden/)) {
- // 判断路径是否改变,menu是否重载
- if (lastPathName !== window.location.pathname || reload) {
- start(true);
- }
- // 判断menu位置
- const winWidth = document.documentElement.clientWidth;
- const winHeight = document.documentElement.clientHeight;
- const x = e.clientX;
- const y = e.clientY;
- const classname1 = winWidth / 2 - x > 0 ? 'le-md-right' : 'le-md-left';
- const classname2 = winHeight / 2 - y > 0 ? 'le-md-bottom' : 'le-md-top';
- $menu.className = `${classname1} ${classname2}`;
- } else {
- $menu.className += ' hidden';
- }
- }
- // 插入样式表
- function style() {
- const style = document.createElement('style');
- style.innerHTML = `
- .le-md {
- position: fixed;
- top: 16%;
- left: 90%;
- z-index: 999;
- }
- .le-md-btn {
- display: block;
- font-size: 14px;
- text-transform: uppercase;
- width: 60px;
- height: 60px;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- border-radius: 50%;
- color: #fff;
- text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.8);
- border: 0;
- background: hsla(230, 50%, 50%, 0.6);
- -webkit-animation: pulse 1s infinite alternate;
- animation: pulse 1s infinite alternate;
- -webkit-transition: background 0.4s, margin 0.2s;
- -o-transition: background 0.4s, margin 0.2s;
- transition: background 0.4s, margin 0.2s;
- text-align: center;
- line-height: 60px;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- cursor: move;
- }
- .le-md-btn:after {
- background: rgba(0, 0, 0, 0.05);
- border-radius: 50%;
- bottom: -22.5px;
- content: "";
- display: block;
- height: 0;
- margin: 0 auto;
- left: 0;
- position: absolute;
- right: 0;
- width: 40px;
- -webkit-transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
- -o-transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
- transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
- -webkit-animation: shadow 1s infinite alternate;
- animation: shadow 1s infinite alternate;
- }
- .le-md-btn:hover {
- background: hsla(220, 50%, 47%, 1);
- margin-top: -1px;
- -webkit-animation: none;
- animation: none;
- -webkit-box-shadow: inset -5px -10px 1px hsla(220, 50%, 42%, 1);
- box-shadow: inset -5px -10px 1px hsla(220, 50%, 42%, 1);
- }
- .le-md-btn:hover:after {
- -webkit-animation: none;
- animation: none;
- height: 10px;
- }
- .le-md-btn-hidden{
- display: none;
- -webkit-animation: none;
- animation: none;
- }
- .hidden {
- height: 0 !important;
- min-height: 0 !important;
- border: 0 !important;
- }
- .le-md-left {
- right: 0;
- margin-right: 100px;
- }
- .le-md-right {
- left: 0;
- margin-left: 100px;
- }
- .le-md-top {
- bottom: 0;
- }
- .le-md-bottom {
- top: 0;
- }
- .le-md > ul {
- width: 200px;
- min-width: 100px;
- max-width: 1000px;
- list-style: none;
- position: absolute;
- overflow: auto;
- -webkit-transition: min-height 0.4s;
- -o-transition: min-height 0.4s;
- transition: min-height 0.4s;
- min-height: 50px;
- height: auto;
- max-height: 700px;
- resize: both;
- padding-right: 10px;
- }
- .le-md > ul::-webkit-scrollbar {
- width: 8px;
- height: 1px;
- }
- .le-md > ul::-webkit-scrollbar-thumb {
- border-radius: 8px;
- -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
- box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
- background-color: #96C2F1;
- background-image: linear-gradient(
- 45deg,
- rgba(255, 255, 255, 0.2) 25%,
- transparent 25%,
- transparent 50%,
- rgba(255, 255, 255, 0.2) 50%,
- rgba(255, 255, 255, 0.2) 75%,
- transparent 75%,
- transparent
- );
- }
- .le-md > ul::-webkit-scrollbar-track {
- -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
- box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
- border-radius: 8px 8px 0 0;
- background: #EFF7FF;
- }
- .le-md > ul a:hover {
- background: #fff;
- border-left: 1em groove #0099CC !important;
- }
- .le-md > ul a {
- text-decoration: none;
- font-size: 1em;
- color: #909399;
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
- display: block;
- white-space: nowrap;
- -o-text-overflow: ellipsis;
- text-overflow: ellipsis;
- overflow: hidden;
- padding: 5px 10px;
- border-bottom: 0.5em solid #eee;
- -webkit-transition: 0.4s all;
- -o-transition: 0.4s all;
- transition: 0.4s all;
- border-left: 0.5em groove #e2e2e2;
- border-right: 1px solid #e2e2e2;
- border-top: 1px solid #e2e2e2;
- background: #f4f4f5;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
- border-radius: 0 0 5px 5px;
- }
- @-webkit-keyframes pulse {
- 0% {
- margin-top: 0;
- }
- 100% {
- margin-top: 6px;
- -webkit-box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
- box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
- }
- }
- @keyframes pulse {
- 0% {
- margin-top: 0;
- }
- 100% {
- margin-top: 6px;
- -webkit-box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
- box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
- }
- }
- @-webkit-keyframes shadow {
- to {
- height: 16px;
- }
- }
- @keyframes shadow {
- to {
- height: 16px;
- }
- }
- .le-md li.le-md-title-active a{
- background: linear-gradient(-135deg, #ffcccc 0.6em, #fff 0);
- }
- .le-md li.le-md-title-active.le-md-title-active-first a{
- background: linear-gradient(-135deg, #ff9999 0.6em, #fff 0);
- color: #000;
- font-weight: 700;
- }
- `;
- document.head.appendChild(style);
- }
- // 拖动事件
- function dragEle(ele) {
- ele.onmousedown = event => {
- // 鼠标相对dom坐标
- let eleX = event.offsetX;
- let eleY = event.offsetY;
- let count = 0;
- window.document.onmousemove = e => {
- //防止误触移动
- if (count > 9) {
- moveStatus = true;
- }
- // dom相对win坐标
- let winX = e.clientX;
- let winY = e.clientY;
- // 实际坐标
- let x = winX - eleX;
- let y = winY - eleY;
- // win长宽
- let winWidth = document.documentElement.clientWidth;
- let winHeight = document.documentElement.clientHeight;
- // 转化成百分比
- ele.parentNode.style.left = (x / winWidth).toFixed(3) * 100 + '%';
- ele.parentNode.style.top = (y / winHeight).toFixed(3) * 100 + '%';
- count++;
- };
- };
- ele.onmouseup = () => {
- window.document.onmousemove = null;
- };
- ele.onmouseout = () => {
- window.document.onmousemove = null;
- };
- }
- // 执行, flag 是否部分重载
- function start(flag) {
- // 初始化
- reload = false;
- // 获取链接
- const host = window.location.host;
- lastPathName = window.location.pathname;
- // 获取相应的容器dom
- let $content = null;
- let list = [];
- if (host === 'github.com') {
- //github home / wiki
- const $parent = document.getElementById('readme') || document.getElementById('wiki-body');
- $content = $parent && $parent.getElementsByClassName('markdown-body')[0];
- // 标题dom高度
- const $boxTitle = ($parent && $parent.parentElement) ? $parent.parentElement.getElementsByClassName('js-sticky')[0] : null;
- titleHeight = $boxTitle ? $boxTitle.offsetHeight + 2 : 0;
- // 监听github dom的变化
- // !$menu && domChangeListener(document.getElementById('js-repo-pjax-container'), start);
- !$menu && window.addEventListener('pjax:complete', start);
- } else if (host === 'gitee.com') {
- //码云 home
- const $parent = document.getElementById('tree-content-holder');
- $content = $parent && $parent.getElementsByClassName('markdown-body')[0];
- // 监听gitee dom的变化
- !$menu && domChangeListener(document.getElementById('tree-holder'), start);
- } else if (host === 'www.npmjs.com') {
- // npmjs.com
- const $parent = document.getElementById('readme');
- $content = $parent ? $parent : null;
- } else {
- // 检测是否符合md格式
- $content = checkMd();
- }
- // 获取子级
- const $children = $content ? $content.children : [];
- for (let $dom of $children) {
- const tagName = $dom.tagName;
- const lastCharAt = +tagName.charAt(tagName.length - 1);
- // 获取Tag h0-h9
- if (tagName.length === 2 && tagName.startsWith('H') && !isNaN(lastCharAt)) {
- // 获取value
- const value = $dom.innerText.trim();
- // 新增容错率
- const $a = $dom.getElementsByTagName('a')[0];
- if ($a) {
- // 获取锚点
- const href = $a.getAttribute('href');
- // 获取offsetTop
- const offsetTop = getTop($a)
- list.push({
- type: lastCharAt,
- value,
- href,
- offsetTop
- });
- }
- }
- }
- // 清空容器,不存在则创建
- if ($menu) {
- const list = [...$menu.childNodes];
- list.forEach(i => $menu.removeChild(i));
- } else {
- createDom();
- }
- if (!$menu || !$button) {
- console.warn('md文件目录化 脚本初始化失败');
- return false;
- }
- // 隐藏菜单
- if (!flag) {
- $menu.className = 'hidden';
- }
- //是否存在
- if (list.length) {
- // 生成菜单
- for (let i of list) {
- const li = document.createElement('li');
- li.setAttribute('data-offsetTop', i.offsetTop)
- const a = document.createElement('a');
- a.href = i.href;
- a.title = i.value;
- 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);`;
- a.innerText = i.value;
- li.appendChild(a);
- $menu.appendChild(li);
- // 是否不符合规范
- if (!i.value) {
- reload = true;
- }
- }
- // 提供关闭入口
- if (reload) {
- const li = document.createElement('li');
- 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>`;
- // 添加事件
- li.onclick = function () {
- $main.remove();
- };
- $menu.appendChild(li);
- }
- // 设置按钮样式
- $button.setAttribute('class', 'le-md-btn');
- } else {
- // 设置按钮样式
- $button.setAttribute('class', 'le-md-btn le-md-btn-hidden');
- }
- }
- /**
- * @Description 监听指定dom发现变化事件
- * @author lecoler
- * @date 2020/7/14
- * @param dom
- * @param fun 回调 (MutationRecord[],MutationObserver)
- * @param opt 额外参数
- * @return MutationObserver
- */
- function domChangeListener(dom, fun, opt = {}) {
- if (!dom) return null;
- const observe = new MutationObserver(fun);
- observe.observe(dom, Object.assign({
- childList: true,
- attributes: true,
- }, opt));
- return observe;
- }
- /**
- * @Description 判断是否符合格式的md
- * @author lecoler
- * @date 2020/9/14
- * @return DOM
- */
- function checkMd() {
- // 缓存
- let tmp = [];
- // 是否存在h1 h2 h3 h4 h5 ...标签,同时他们父级相同
- for (let i = 1; i < 7; i++) {
- let list = document.body.getElementsByTagName(`h${i}`);
- // 获取父级
- for (let i = 0; i < list.length; i++) {
- const parent = list[i].parentElement;
- const item = tmp.filter(j => j && j['ele'].isEqualNode(parent))[0];
- if (item) {
- item.count += 1;
- } else {
- tmp.push({
- ele: parent,
- count: 1,
- });
- }
- }
- }
- // 排序
- tmp.sort((a, b) => b.count - a.count);
- // 获取出现次数最高父级 返回
- return tmp.length ? tmp[0]['ele'] : null;
- }
- // 监听Windows滚动事件
- function onScrollEvent() {
- const fun = debounce(updateTitleActive, 500)
- // 判断原页面是否存在滚动事件监听,存在则合并,否则新建
- const oldFun = window.onscroll
- // 存在
- if (oldFun && oldFun.constructor === Function) {
- window.onscroll = function () {
- // 触发原页面事件
- oldFun.call(this)
- // 刷新标题 active 状态
- fun()
- }
- } else {
- window.onscroll = function () {
- // 刷新标题 active 状态
- fun()
- }
- }
- }
- /**
- * @Description 防抖动
- * @author lecoler
- * @date 2020/7/1
- * @param func<Function>
- * @param time<number>
- * @return Function
- */
- function debounce(func, time) {
- let context, args, timeId, timestamp
- function timeout() {
- const now = Date.now() - timestamp
- if (now >= 0 && now < time) {
- timeId = setTimeout(timeout, time - now)
- } else {
- timeId = null
- func.apply(context, args)
- }
- }
- function action() {
- context = this
- args = arguments
- timestamp = Date.now()
- if (!timeId) timeId = setTimeout(timeout, time)
- }
- return action
- }
- // 更新标题active状态
- function updateTitleActive() {
- // 获取目前页面scrollTop
- const ScrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0
- const scrollTop = ScrollTop + titleHeight
- const offsetHeight = document.documentElement.clientHeight || document.body.clientHeight || 0
- const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight || 0
- // 存在菜单
- if ($menu) {
- const list = $menu.children || []
- for (let i = 0; i < list.length; i++) {
- const val = list[i].getAttribute('data-offsetTop')
- const nextVal = list[i + 1] ? list[i + 1].getAttribute('data-offsetTop') : scrollHeight
- // 排他
- list[i].removeAttribute('class')
- // 肉眼可见部分,标题高亮
- if (scrollTop <= val && val <= offsetHeight + scrollTop) {
- list[i].className = 'le-md-title-active'
- }
- // 正在阅读部分,标题高亮
- if (scrollTop >= val && nextVal > scrollTop) {
- list[i].className = 'le-md-title-active le-md-title-active-first'
- }
- }
- }
- }
- /**
- * @describe 获取dom元素距离body的offsetTop
- * @author lecoler
- * @date 21-1-8
- * @param $dom<Node>
- * @return Number
- */
- function getTop($dom, val = 0) {
- if (!$dom) return val
- const offsetTop = $dom.offsetTop || 0
- return getTop($dom.offsetParent, offsetTop + val)
- }
- try {
- document.onreadystatechange = function () {
- if (document.readyState === 'complete') {
- start();
- // 监听滚动
- onScrollEvent()
- }
- };
- } catch (e) {
- console.error('github、码云 md文件目录化 脚本异常报错:');
- console.error(e);
- console.error('请联系作者修复解决,https://github.com/lecoler/md-list');
- }
- })();