Github, code cloud md file directory

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.13
// @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         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;

    // 初始化按钮
    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: 12%;
            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: #99CCFF;
            border-left: 1em groove #0099CC !important;
        }
        .le-md > ul a {
            text-decoration: none;
            font-size: 1em;
            color: #333;
            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: #fff;
            -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{
            font-weight: 600;
            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];
            // 监听github dom的变化
            !$menu && domChangeListener(document.getElementById('js-repo-pjax-container'), 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 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');
    }
})();