Greasy Fork is available in English.

springfox-swagger2

swagger-ui 展开、折叠、搜索、方法定位

// ==UserScript==
// @name         springfox-swagger2
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  swagger-ui 展开、折叠、搜索、方法定位
// @author       dairong
// @include      *://*/swagger-ui.html
// @include      *://*/swagger-ui.html#/*
// @include      *://*/*swagger-ui.html*
// @grant        none
// ==/UserScript==

(function (config) {
    'use strict';

    // 展开状态  [ 展开true | 收起false ]
    let state = false
    // 操作按钮
    let myButton
    // 控制器集合
    let operations
    // url hash
    let hash
    // api 数据
    let apiData = []

    function getJavaTemplate({
        path,
        pathCamelCase,
        description,
        url
    }) {
        // java 字符模板
        let javaTemplate = `
    ------------------------------------------------
        ${pathCamelCase}:
        serviceId: NONE
        path: ${path}
        ------------------------------------------------
        @PostMapping(value = "${path}")
        @Operation(description = "${description}", externalDocs = @ExternalDocumentation(description = "${description}",
        url = "${url}"
        ))
        public BaseResult<?> ${pathCamelCase}(HttpServletRequest request) {
        return unifiedInvoker.restInvoke("${pathCamelCase}", request);
        }
    ------------------------------------------------
`
        return javaTemplate
    }

    // 生成 toast
    function getToastScript() {
        let toastString = 'function _toast(msg,duration){duration=isNaN(duration)?3000:duration;var m=document.createElement("div");m.innerHTML=msg;m.style.cssText="font-family:siyuan;max-width:60%;min-width: 150px;padding:0 14px;height: 40px;color: rgb(255, 255, 255);line-height: 40px;text-align: center;border-radius: 4px;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999999;background: rgba(0, 0, 0,.7);font-size: 16px;";document.body.appendChild(m);setTimeout(function(){var d=0.5;m.style.webkitTransition="-webkit-transform "+d+"s ease-in, opacity "+d+"s ease-in";m.style.opacity="0";setTimeout(function(){document.body.removeChild(m)},d*1000)},duration)};'
        let script = document.createElement("script");
        script.innerHTML = toastString;
        return script;
    }

    // 生成 button
    function getButtonElement(text, color = '#49cc90') {
        let buttonString = `<div class="mybutton" style="background:${color};font-size: 14px;font-weight: 700;min-width: 80px;padding: 6px 15px;text-align: center;border-radius: 3px;text-shadow: 0 1px 0 rgba(0,0,0,.1);color: #fff;">${text}</div>`;
        const buttonElement = document.createElement("div");
        buttonElement.style = 'display: inline-block; margin-left: 6px;'
        buttonElement.innerHTML = buttonString
        return buttonElement;
    }

    // 操作按钮
    function getBottons() {
        // copy button
        const copyButton = getButtonElement(config.copyButton.text, config.copyButton.color);
        copyButton.onclick = copyBottonClick;
        // template button
        const templateButton = getButtonElement(config.templateButton.text, config.templateButton.color);
        templateButton.onclick = templateBottonClick;

        const buttonsElement = document.createElement("div");
        // buttonsElement.style = 'background: blue;'
        // buttonsElement.appendChild(templateButton);
        buttonsElement.appendChild(copyButton);
        return buttonsElement
    }

    // <script src="https://wzrd.in/standalone/copy-to-clipboard@latest" async></script>
    function getClipboardScript() {
        let script = document.createElement("script");
        script.type = "text/javascript";
        script.innerHTML = `!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).copyToClipboard=e()}}(function(){return function(){return function e(t,n,o){function r(a,i){if(!n[a]){if(!t[a]){var u="function"==typeof require&&require;if(!i&&u)return u(a,!0);if(c)return c(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var s=n[a]={exports:{}};t[a][0].call(s.exports,function(e){return r(t[a][1][e]||e)},s,s.exports,e,t,n,o)}return n[a].exports}for(var c="function"==typeof require&&require,a=0;a<o.length;a++)r(o[a]);return r}}()({1:[function(e,t,n){"use strict";var o=e("toggle-selection"),r="Copy to clipboard: #{key}, Enter";t.exports=function(e,t){var n,c,a,i,u,l,s=!1;t||(t={}),n=t.debug||!1;try{if(a=o(),i=document.createRange(),u=document.getSelection(),(l=document.createElement("span")).textContent=e,l.style.all="unset",l.style.position="fixed",l.style.top=0,l.style.clip="rect(0, 0, 0, 0)",l.style.whiteSpace="pre",l.style.webkitUserSelect="text",l.style.MozUserSelect="text",l.style.msUserSelect="text",l.style.userSelect="text",document.body.appendChild(l),i.selectNode(l),u.addRange(i),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");s=!0}catch(o){n&&console.error("unable to copy using execCommand: ",o),n&&console.warn("trying IE specific stuff");try{window.clipboardData.setData("text",e),s=!0}catch(o){n&&console.error("unable to copy using clipboardData: ",o),n&&console.error("falling back to prompt"),c=function(e){var t=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return e.replace(/#{\s*key\s*}/g,t)}("message"in t?t.message:r),window.prompt(c,e)}}finally{u&&("function"==typeof u.removeRange?u.removeRange(i):u.removeAllRanges()),l&&document.body.removeChild(l),a()}return s}},{"toggle-selection":2}],2:[function(e,t,n){t.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],o=0;o<e.rangeCount;o++)n.push(e.getRangeAt(o));switch(t.tagName.toUpperCase()){case"INPUT":case"TEXTAREA":t.blur();break;default:t=null}return e.removeAllRanges(),function(){"Caret"===e.type&&e.removeAllRanges(),e.rangeCount||n.forEach(function(t){e.addRange(t)}),t&&t.focus()}}},{}]},{},[1])(1)});`
        return script
    }

    // 向页面添加 toast 插件
    function appendToast() {
        let toastScript = getToastScript();
        document.getElementsByTagName('head')[0].appendChild(toastScript);
    }

    // 向页面添加 copy 插件
    function appendClipboard() {
        let clipboardScript = getClipboardScript();
        document.getElementsByTagName('head')[0].appendChild(clipboardScript);
    }

    // 向页面添加 button
    function appendButton(ele) {
        const baseElement = ele || document;
        const paths = baseElement.querySelectorAll('.opblock-summary');
        // 
        paths.forEach(item => {
            let bottons = getBottons();
            item.appendChild(bottons);
        })
    }

    // copy 按钮点击
    function copyBottonClick(e) {
        try {
            // const title = document.querySelector('.information-container .title').innerText
            const pathElement = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('.opblock-summary-path a span');
            const path = pathElement.innerText;
            window.copyToClipboard(path);
            _toast('复制成功', 1200);
        } catch (error) {
            console.error(error)
        }
        e.stopPropagation();
    }

    function templateBottonClick(e) {
        try {
            const pathElement = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('.opblock-summary-path a span');
            const descElement = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('.opblock-summary-description');
            let path = pathElement.innerText;
            let pathCamelCase = path2CamelCase(path);
            let description = descElement.innerText
            let url = decodeURIComponent(location.href);
            let data = getJavaTemplate({
                path,
                pathCamelCase,
                description,
                url
            });
            window.copyToClipboard(data);
            _toast('复制成功', 1200);
        } catch (error) {
            console.error(error)
        }
        e.stopPropagation();
    }

    function path2CamelCase(path) {
        if (path) {
            let words = path.split('/').filter(item => item);
            let pathCamelCase = words.reduce((t, c, i) => {
                return t += ( i === 0 ? c.charAt(0).toLowerCase() : c.charAt(0).toUpperCase() ) + c.substring(1);
            }, '')
            return pathCamelCase
        }
        return path
    }

    // controller 点击
    function opblockTagClick(e) {
        // 使用 event.target 会存在bug, 真正的点击到的元素可能是 opblocktag 内部的 元素
        // const currentElement = e.target
        // const controllerElement = currentElement.parentElement
        // const className = controllerElement.className
        // console.log(this);
        let opblockTag = this.parentElement
        nextTick(() => {
            appendButton(opblockTag);
        });
    }

    // 兼容 controller 展开、折叠 追加的按钮消失问题
    function handleOpblockTagClick() {
        const opblockTags = document.querySelectorAll('.opblock-tag');
        opblockTags.forEach(item => {
            // fix bug
            item.onclick = opblockTagClick.bind(item);
        })
    }

    // 数据匹配
    function match(val) {
        let valLowerCase = val.toLocaleLowerCase();
        return apiData.filter(item => {
            let pathLowerCase = item.path;
            let summaryLowerCase = item.summary;
            return pathLowerCase.toLocaleLowerCase().includes(valLowerCase) || summaryLowerCase.toLocaleLowerCase().includes(valLowerCase)
        })
    }

    // 模糊搜索(支持方法说明、映射路径)
    function search(e) {
        let val = e.target.value.trim()
        let list = document.getElementById('operList')
        if (val === "") {
            list.classList.add("hidden");
        } else {
            // 匹配方法
            let matchPaths = match(val);
            renderLi(matchPaths);
            list.classList.remove("hidden");
        }
    }

    // 渲染查询结果
    function renderLi(matchPaths) {
        let liHtml = ""
        if (matchPaths.length === 0) {
            liHtml =
                `<li class="list-li"><span class="list-li-left">暂无数据</span><span class="list-li-right"></span></li>`
        } else {
            matchPaths.forEach(item => {
                liHtml +=
                    `<li class="list-li" data-operation-id="${item.operationId}"><span class="list-li-left">${item.method} ${item.path}</span><span class="list-li-right">${item.summary}</span></li>`
            })
        }
        document.getElementById("operUl").innerHTML = liHtml
        // 绑定事件
        let lis = document.querySelectorAll('.list-li')
        lis.forEach(element => {
            element.addEventListener('mouseenter', function () {
                this.classList.add('active')
            });
            element.addEventListener('mouseleave', function () {
                this.classList.remove('active')
            });
            element.addEventListener('click', function (e) {
                let item = apiData.find(val => val.operationId === e.currentTarget.dataset.operationId)
                extendMethod(extendController(item.tag), item.tag, item.operationId);
                let operInput = document.getElementById("operInput");
                operInput.value = ''
                operInput.setAttribute('placeholder', `${item.method} ${item.path}`)
            });
        });
    }

    // input 失去焦点
    function inputBlur() {
        nextTick(() => {
            document.getElementById('operList').classList.add("hidden");
        }, 300)
    }

    // 点击事件
    function buttonClick() {
        if (state) {
            doCollapse();
        } else {
            doExtend();
        }
    }

    // 事件注册
    function initEvent() {
        document.getElementById("operInput").addEventListener("input", search);
        document.getElementById("operInput").addEventListener("blur", inputBlur);
        document.getElementById("operButton").addEventListener("click", buttonClick);
    }

    // UI初始化
    function appendSearchUI() {
        // 样式加载
        let commonStyle = `.extends{position:fixed;display:inline-block;right:15px;top:60px;font-size:14px}.oper{display:inline-table;box-shadow:3px 4px 5px 0 #999}.oper-input{background-color:#fff;border:1px solid #dcdfe6;box-sizing:border-box;color:#606266;font-size:inherit;height:40px;line-height:40px;padding:0 15px;width:700px;cursor:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC10lEQVRYR+2WvWtTURTAfyFp0yQmNWlCGiqUDkLGClIcDCiEpIPulkyCDv4N6u6fIOjQ0sV0KUIHQQuKgyEEdCoO0qWUl8S0CpUIiemTc30vPGryPhIxS88S8u459/7u+bw+7OVnOByeabfb34G4g+5Iyz4bq2+rq6sX5+fnlcr6+voX4PJIp9gY2QHomUwGn89HoVAQANnGTn8kNkcA2TWXy7G1tTU5gIWFBWq12uQAQqEQ+/v7kwOQkzVNOwcQD3wGska6Z4D6SKlvMXJVBWYISqUSU1NTynxjYwNd13ei0eitXq9Hu91+Dtz3CmQFKAEvgR/GJqoPmCI5YP5fWlqSA9XS8vKy+q1Wq+zt7V0FVLlI7wLuOgEJQBF4JYrT09N0Op2nwANgKIDoiie63a7af2VlhdnZWcrlMmtra+rbwcEBu7u7d4CyHYQA6KIQi8WIRCI0m016vZ76PswDon9yckI0Gu3vXSwW2d7eJhwOs7i4SDable75FrjpCmBubk55QG7VarXywBsvAPF4nMPDQwUgks/n2dzcdAXwUUIptxcvGAnXApJeAMRO8sIEEC9UKhVXAJckZDJ0zMnXaDQ4PT3tJ90gF54NwVmAQCAgeeAKQGxVHpg37nQ6HB0d2QIMgrJ6wPCka4D3wPVEIkEwGDTD4BlA+oHf7++zaZrmGkCFweoFCUM6nXYqY9t1LwADwyBVMY54BXgt1SMNxczkcQ73mgOinwS+WsPwvwH+CsMkAFQYrNUwDoSmaTvAbadWbF3/Z2EwWvoN4J0XgH4YpCtKdxxVjG7quMEghWfAvXGSUdp4o9F4AfyZzTYyjFC1ZuuActrIuu7lATsM4IKMfFWbyWT/GeYGwnC9PBTMl9VIHhCjPkQqlUKmm53ouk69rt6oKUDGuStxTBLgA3BtWIeUg4+Pj+Up9wm44upUi5IbAFP9IfAImLHY/wKeAI+9Hmzq/wbyQyQw9l5pkQAAAABJRU5ErkJggg==") 12 3,auto!important;outline:0;vertical-align:middle;display:table-cell}.oper-button{background-color:#f5f8fa;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #dcdfe6;border-left:none;height:40px;line-height:1;border-radius:0 4px 4px 0;padding:0 20px;width:120px;white-space:nowrap;outline:0}.list{margin-top:15px;background:#fff;box-shadow:3px 4px 5px 0 #999}.list.hidden{display:none}.list-ul{width:100%;margin:0;padding:0;list-style:none;width:820px;border:1px solid #dcdfe6;max-height:600px;overflow-y:scroll}.list-zw{width:120px}.list-li{padding:10px 15px;border-bottom:1px solid #dcdfe6;overflow:hidden;box-sizing:border-box}.list-li.active{background:#f5f8fa}.list-li-left{width:60%;float:left;white-space:nowrap;word-break:break-all;overflow:hidden;text-overflow:ellipsis;box-sizing:border-box}.list-li-right{box-sizing:border-box;width:40%;float:right;padding-left:15px;white-space:nowrap;word-break:break-all;overflow:hidden;text-overflow:ellipsis;text-align:right}`;
        let styleTextNode = document.createTextNode(commonStyle);
        let styleElement = document.createElement('style');
        styleElement.appendChild(styleTextNode)
        document.body.appendChild(styleElement);
        // 创建根节点
        let rootDiv = document.createElement('div');
        rootDiv.setAttribute("id", "swagger-ui-extends-root");
        document.body.appendChild(rootDiv);

        let rootHtml = `<div class="extends"><div class="oper"><input class="oper-input" id="operInput" type="text" value="" /><button class="oper-button" id="operButton">展开</button></div><div class="list hidden" id="operList"><ul class="list-ul" id="operUl"></ul></div></div>`;
        rootDiv.innerHTML = rootHtml;

        initEvent();
    }

    // 设置按钮文字
    function setMyButtonInnerText(text) {
        myButton = myButton || document.getElementById('operButton');
        myButton.innerText = text
    }

    // 所有控制器按钮
    function getOperationsButtons() {
        if (!operations || operations.length === 0) {
            operations = document.querySelectorAll('.opblock-tag-section')
        }
        return operations
    }

    // 展开
    function doExtend() {
        setMyButtonInnerText(config.loadingText);
        nextTick(() => {
            let operations = getOperationsButtons()
            operations.forEach(btn => {
                let isOpen = btn.className.indexOf('is-open') > -1
                if (!isOpen) {
                    btn.querySelector('.expand-operation').click();
                }
            });
            state = true;
            setMyButtonInnerText(config.collapseText);
        })
    }

    // 收起
    function doCollapse() {
        setMyButtonInnerText(config.loadingText);
        nextTick(() => {
            let operations = getOperationsButtons()
            operations.forEach(btn => {
                let isOpen = btn.className.indexOf('is-open') > -1
                if (isOpen) {
                    btn.querySelector('.expand-operation').click();
                }
            });
            state = false;
            setMyButtonInnerText(config.extendText);
        })
    }

    // next tick 解决加载状态无法及时生效
    function nextTick(fn, time = 50) {
        setTimeout(() => {
            let isFunction = Object.prototype.toString.call(fn) === "[object Function]"
            if (isFunction) {
                fn();
            }
        }, time)
    }

    // 获取查询参数
    function getLoactionHash() {
        // #/auto-msg-controller/checkAutoMsgUsingPOST
        // rdss 自定义 hash
        let reg = /\$.+\$/
        let customHash = reg.exec(window.location.hash)
        if (customHash) {
            hash = window.location.hash
            return {
                type: 3,
                name: decodeURIComponent(customHash[0].replace(/\$/ig, ''))
            }
        }
        hash = decodeURIComponent(window.location.hash)
        let hashArray = hash.split('/').filter(v => v !== '#' && v !== "")
        return {
            type: hashArray.length,
            controllerName: hashArray[0],
            methodName: hashArray[1],
        }
    }

    // 定位控制器
    function extendController(controllerName) {
        let controllerElement = document.getElementById('operations-tag-' + controllerName);
        if (!controllerElement) {
            console.error('未找到页面元素', controllerName)
        }
        let controllerSection = controllerElement.parentElement;
        let isOpen = controllerSection.className.indexOf('is-open') > -1;
        if (!isOpen) {
            controllerElement.querySelector('.expand-operation').click();
        }
        return controllerSection
    }

    // 定位方法
    function extendMethod(controllerSection, controllerName, methodName) {
        let methodElement = controllerSection.querySelector(`#operations-${controllerName}-${methodName}`);
        if (!methodElement) {
            console.error('未找到页面元素', `${controllerName}-${methodName}`)
        }
        let methodIsOpen = methodElement.className.indexOf('is-open') > -1;
        if (!methodIsOpen) {
            let methodHref = methodElement.querySelector('.opblock-summary')
            methodHref.click();
            nextTick(() => {
                methodHref.scrollIntoView();
            })
        } else {
            let methodHref = methodElement.querySelector('.opblock-summary')
            nextTick(() => {
                methodHref.scrollIntoView();
            })
        }
        window.location.hash = hash
    }

    // http get
    function get(url, successCallBack) {
        // 解析数据
        function parseData(responseText) {
            let swaggerInfo = JSON.parse(responseText)
            let paths = swaggerInfo.paths
            let pathKeys = Object.keys(paths)
            pathKeys.forEach(pathKey => {
                // 映射路径
                let pathObjs = paths[pathKey]
                let pathObjKeys = Object.keys(pathObjs)
                pathObjKeys.forEach(methodName => {
                    let pathObj = pathObjs[methodName]
                    // 方法集合
                    apiData.push({
                        path: pathKey,
                        method: methodName,
                        tag: pathObj.tags[0],
                        summary: pathObj.summary,
                        description: pathObj.description,
                        operationId: pathObj.operationId,
                    })
                })
            })
            successCallBack && successCallBack(apiData)
        }
        // 发送 http 请求
        let httpRequest = new XMLHttpRequest();
        httpRequest.onreadystatechange = function () {
            if (httpRequest.readyState === XMLHttpRequest.DONE) {
                if (httpRequest.status === 200) {
                    try {
                        parseData(httpRequest.responseText)
                    } catch (error) {
                        console.error(`数据处理异常:`, httpRequest.responseText);
                    }
                } else {
                    console.error(`请求失败, 检测请求地址是否有效【${url}】`);
                }
            }
        };
        httpRequest.open('GET', url);
        httpRequest.send();
    }

    // 获取 swagger 数据
    function initUI(delegationCustom) {
        // 2、加载 api 数据,挂载查询组件
        get(`${window.location.origin}/v2/api-docs`, (data) => {
            // console.log('api 数据准备就绪!', apiData);
            if (delegationCustom) {
                // 自定义 hash 处理
                try {
                    console.log('处理自定义hash', delegationCustom);
                    let matchPath = apiData.find(obj => obj.path.toLocaleLowerCase().includes(delegationCustom.toLocaleLowerCase()))
                    hash = `#/${matchPath.tag}/${matchPath.operationId}`
                    extendMethod(extendController(matchPath.tag), matchPath.tag, matchPath.operationId);
                } catch (error) {
                    console.error('处理自定义hash异常', error)
                }

            }
            appendSearchUI();
        })
    }

    // 初始化组件
    function initSwaggerExtend() {
        // 1、type [ 0无, 1controller, 2method, 3name ]
        let hashResult = getLoactionHash()
        const {
            type,
            controllerName,
            methodName,
            name
        } = hashResult
        // console.log('获取 hash', hashResult);
        // 委托处理自定义 hash(依赖 api 数据)
        let delegationCustom
        switch (type) {
            case 1:
                // 中文无法自动展开
                extendController(controllerName);
                break;
            case 2:
                // 定位到方法
                extendMethod(extendController(controllerName), controllerName, methodName);
                break;
            case 3:
                // 自定义 hash 处理
                delegationCustom = name
                break;
            default:
                // 展开所有控制器
                if (config.autoExtend) {
                    setMyButtonInnerText(config.loadingText);
                    nextTick(doExtend)
                }
                break;
        }
        // 2、添加 search input element
        initUI(delegationCustom);
        // 3、添加 toast script
        appendToast();
        // 4、添加 copy script
        appendClipboard();
        // 5、添加按钮
        appendButton();
        // 6、controller 绑定点击事件
        handleOpblockTagClick();

    }

    // swagger 异步加载,定时器捕获页面元素
    let timer = setInterval(() => {
        let operations = getOperationsButtons();
        if (operations.length === 0) {
            // 页面没有加载完成
            return;
        }
        clearInterval(timer);
        console.log('swagger-ui 组件加载开始!');
        initSwaggerExtend();
        console.log('swagger-ui 组件加载完成!');
    }, 500);

})({
    autoExtend: false,
    extendText: "展开",
    collapseText: "收起",
    loadingText: "loading...",
    copyButton: {
        text: '复制路径',
        color: '#49cc90'
    },
    templateButton: {
        text: '复制模板',
        color: '#49cc90'
    },
});