- // ==UserScript==
- // @name swagger-toolkit
- // @namespace https://github.com/SublimeCT/greasy_monkey_scripts
- // @version 1.2.0
- // @description Swagger 站点工具脚本 💪 | 保存浏览历史 🕘 | 显示收藏夹 ⭐️ | 点击 path 快速定位 🎯 | 快速复制 API path 🔗
- // @description:en Swagger Toolkit Script 💪 | save history in sidebar 🕘 | has favorites list in sidebar ⭐️ | click path(in sidebar) to jump 🎯 | copy(hover API) API path 🔗
- // @note v1.0.1 增加当前页是不是 swagger 构建的文档判断; 自动展开所有 tag, 以定位到对应的 API;
- // @note v1.1.0 增加复制 API path 功能
- // @note v1.1.1 fix: 修复增加历史记录时将 toolkit-btn-group 内容一起加进去的问题
- // @note v1.2.0 feat: 增加多语言(英语)支持
- // @author Sven
- // @icon https://static1.smartbear.co/swagger/media/assets/swagger_fav.png
- // @match *://*/docs/index.html
- // @match *://*/docs/api/index.html
- // @match https://petstore.swagger.io
- // @grant none
- // ==/UserScript==
-
- ; (() => {
- // @require file:///Users/test/projects/greasy_monkey_scripts/swagger_toolkit.js
- const TIMES = 30
- let current = 0
- let isLoaded = false
- const interval = setInterval(() => {
- if (++current >= TIMES) {
- clearInterval(interval)
- return
- }
- const item = document.querySelector('.opblock-tag')
- const swaggerAPI = window.SwaggerUIBundle
- if (!item || !swaggerAPI) return
- if (!isLoaded) {
- // 首先展开所有 tag, 否则无法定位
- const notOpenTags = document.querySelectorAll('.opblock-tag[data-is-open=false]') || []
- for (const tag of Array.from(notOpenTags)) {
- tag.click()
- }
- // 增加监听事件
- const wrapper = document.querySelector('.swagger-ui')
- wrapper.addEventListener('click', evt => {
- // 点击接口标题时在当前 URL 中加入锚点
- const linkTitleDom = evt.target.closest('.opblock-summary')
- if (linkTitleDom) {
- const linkDom = linkTitleDom.parentNode
- const isOpen = !linkDom.classList.contains('is-open')
- const hash = isOpen ? linkDom.id : ''
- if (hash) location.hash = hash
- return
- }
- // 点击接口中的 Model 时同步展开下方数据结构
- const modelLinkDom = evt.target.closest('ul.tab')
- if (modelLinkDom && evt.target.innerText.trim() === 'Model') {
- setTimeout(() => {
- const icons = modelLinkDom.nextElementSibling.querySelectorAll('.model-toggle.collapsed')
- if (icons.length) icons[icons.length - 1].click()
- }, 300)
- return
- }
- })
- if (location.hash) {
- observeHash()
- window.addEventListener('hashchange', observeHash)
- }
- isLoaded = true
- return
- }
- }, 300);
- const observeHash = evt => {
- const linkedDom = document.getElementById(location.hash.length > 0 ? location.hash.substr(1) : '')
- if (linkedDom) {
- const isOpen = linkedDom.classList.contains('is-open')
- linkedDom.scrollIntoView()
- if (!isOpen) linkedDom.querySelector('.opblock-summary').click()
- console.log('scroll into view: ', linkedDom, linkedDom.querySelector('.opblock-summary'))
- }
- }
- class Sheets {
- static sheets = `
- body {
- --row-width: 13vw;
- --row-min-width: 245px;
- --row-title-font-size: 14px;
- --body-wrapper-width: 80vw;
- --body-wrapper-margin-right: 3vw;
- --body-wrapper-min-width: 800px;
- --body-btn-group-width: 20px;
- }
-
- /* 应用于 Copy input */
- .toolkit-hidden { width: 1; height: 1; }
-
- /* 接口信息部分样式 */
- #swagger-ui .opblock .toolkit-path-btn-group { margin-left: 10px; display: none; }
- #swagger-ui .opblock:hover .toolkit-path-btn-group { display: block; }
- #swagger-ui .opblock .toolkit-path-btn-group a { text-decoration: none; }
-
- /* 页面内容主体布局 */
- #swagger-ui div.topbar { display: flex; justify-content: flex-end; }
- #swagger-ui div.topbar .wrapper { margin: 0; width: var(--body-wrapper-width); min-width: var(--body-wrapper-min-width); margin-right: var(--body-wrapper-margin-right) }
- #swagger-ui div.swagger-ui { display: flex; justify-content: flex-end; }
- #swagger-ui div.swagger-ui .wrapper { margin: 0; width: var(--body-wrapper-width); min-width: var(--body-wrapper-min-width); margin-right: var(--body-wrapper-margin-right) }
-
- /* sidebar part */
- #swagger-toolkit-sidebar {
- width: var(--row-width);
- min-width: var(--row-min-width);
- display: flex;
- position: fixed;
- top: 0;
- left: 0;
- height: 100vh;
- flex-direction: column;
- justify-content: space-between;
- background-color: #FAFAFA;
- border-right: 1px solid #c4d6d6;
- }
- #swagger-toolkit-sidebar .list { width: 100%; }
- #swagger-toolkit-sidebar .list > header { font-size: 18px; background-color: #999; }
- #swagger-toolkit-sidebar .list > header > .title { color: #FFF; text-align: center; font-weight: 200; }
- #swagger-toolkit-sidebar .row { display: flex; padding-bottom: 5px; width: 100%; cursor: pointer; text-decoration: none; }
- #swagger-toolkit-sidebar .row.method-DELETE { background-color: rgba(249,62,62,.1); }
- #swagger-toolkit-sidebar .row.method-DELETE:hover { background-color: rgba(249,62,62,.5); }
- #swagger-toolkit-sidebar .row.method-GET { background-color: rgba(97,175,254,.1); }
- #swagger-toolkit-sidebar .row.method-GET:hover { background-color: rgba(97,175,254,.5); }
- #swagger-toolkit-sidebar .row.method-POST { background-color: rgba(73,204,144,.1); }
- #swagger-toolkit-sidebar .row.method-POST:hover { background-color: rgba(73,204,144,.5); }
- #swagger-toolkit-sidebar .row.method-PUT { background-color: rgba(252,161,48,.1); }
- #swagger-toolkit-sidebar .row.method-PUT:hover { background-color: rgba(252,161,48,.5); }
- #swagger-toolkit-sidebar .row.method-PATCH { background-color: rgba(80,227,194,.1); }
- #swagger-toolkit-sidebar .row.method-PATCH:hover { background-color: rgba(80,227,194,.5); }
-
- #swagger-toolkit-sidebar .row .description { color: #333; font-size: 14px; width: calc(var(--row-width) - var(--body-btn-group-width)); min-width: calc(var(--row-min-width) - var(--body-btn-group-width)); }
- #swagger-toolkit-sidebar .row .method { display: flex; line-height: 45px; min-width: 64px; }
- #swagger-toolkit-sidebar .row .path > a { color: #409EFF; }
-
- #swagger-toolkit-sidebar .row .btn-group { font-size: 12px; }
- #swagger-toolkit-sidebar .row .btn-group > a { text-decoration: none; display: block; }
- #swagger-toolkit-sidebar .row .btn-group > a:hover { font-size: 14px; }
-
- /* helper */
- .tool-text-size-fixed { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- `
- static inject() {
- const sheet = document.createTextNode(Sheets.sheets)
- const el = document.createElement('style')
- el.id = 'swagger-toolkit-sheets'
- el.appendChild(sheet)
- document.getElementsByTagName('head')[0].appendChild(el)
- }
- }
- class LinkStore {
- key = ''
- path = ''
- method = ''
- description = '' // 接口名
- id = ''
- createdat = 0
- static MAX_LENGTH = 10
- static save(row, key) {
- const store = new LinkStore()
- store.id = row.id
- store.key = key
- store.method = row.querySelector('.opblock-summary-method').innerText
- store.path = row.querySelector('.opblock-summary-path > a').innerText
- store.description = row.querySelector('.opblock-summary-description').innerText
- LinkStore.add(key, store)
- }
- static add(key, store, filterRepeat) {
- let data = LinkStore.getStore(key)
- if (filterRepeat) {
- for (const row of data) {
- if (row.id === store.id && store.path === store.path) return false
- }
- }
- data.unshift(store)
- if (data.length > LinkStore.MAX_LENGTH) data = data.slice(0, LinkStore.MAX_LENGTH)
- localStorage.setItem(key, JSON.stringify(data))
- }
- static remove(key, index) {
- let data = LinkStore.getStore(key)
- data.splice(index, 1)
- localStorage.setItem(key, JSON.stringify(data))
- }
- static getStore(key) {
- let store = []
- try {
- const _store = localStorage.getItem(key)
- if (_store) store = JSON.parse(_store)
- } catch (err) {
- console.error(err)
- }
- return store
- }
- }
- class Pane {
- dom = null
- localKey = null
- title = null
- placeholder = '暂无数据'
- placeholder_en = 'no data'
- btnSave = '收藏'
- btnSave_en = 'add to favorites'
- btnRemove = '删除'
- btnRemove_en = 'remove'
- enableMarkBtn = false
- /**
- * 生成或更新当前 Pane
- * @description 将生成 `.list>(header>.title)+(a.row>(.method+.contents>(.description+a.path)))`
- */
- generateDom(isUpdate) {
- if (isUpdate) this.dom.innerHTML = ''
- const list = isUpdate ? this.dom : document.createElement('div')
- list.classList.add('list')
- list.classList.add(this.localKey)
- list.setAttribute('data-key', this.localKey)
- // 添加 header
- const header = document.createElement('header')
- const title = document.createElement('div')
- title.classList.add('title')
- title.innerText = this.getLabelByLanguage('title')
- list.appendChild(header)
- header.appendChild(title)
- // 添加数据
- const data = LinkStore.getStore(this.localKey)
- for (const dataRow of data) {
- const row = document.createElement('a')
- row.href = '#' + dataRow.id
- row.setAttribute('data-row', JSON.stringify(dataRow))
- const method = document.createElement('div')
- method.innerText = dataRow.method
- const contents = document.createElement('div')
- const description = document.createElement('div')
- description.innerText = dataRow.description
- const path = document.createElement('div')
- const pathLink = document.createElement('a')
- pathLink.innerText = dataRow.path
- pathLink.href = '#' + dataRow.id
- const btnGroup = document.createElement('div')
- const markBtn = document.createElement('a')
- if (this.enableMarkBtn) {
- markBtn.href = 'javascript:;'
- markBtn.setAttribute('title', this.getLabelByLanguage('btnSave'))
- markBtn.innerText = '⭐️'
- }
- const deleteBtn = document.createElement('a')
- deleteBtn.href = 'javascript:;'
- deleteBtn.setAttribute('title', this.getLabelByLanguage('btnRemove'))
- deleteBtn.innerText = '✖️'
-
- row.classList.add('row')
- row.classList.add('method-' + dataRow.method)
- method.classList.add('method')
- contents.classList.add('contents')
- description.classList.add('description')
- description.classList.add('tool-text-size-fixed')
- path.classList.add('path')
- btnGroup.classList.add('btn-group')
- if (this.enableMarkBtn) markBtn.classList.add('btn-mark')
- deleteBtn.classList.add('btn-delete')
-
- path.appendChild(pathLink)
- contents.appendChild(description)
- contents.appendChild(path)
- // row.appendChild(method)
- row.appendChild(contents)
- row.appendChild(btnGroup)
- btnGroup.appendChild(deleteBtn)
- if (this.enableMarkBtn) btnGroup.appendChild(markBtn)
- list.appendChild(row)
- }
- if (data.length === 0) list.appendChild(this.getPlaceholderDom())
- this.dom = list
- if (typeof this.afterGenerageDom === 'function') this.afterGenerageDom()
- return list
- }
- getPlaceholderDom() {
- const dom = document.createElement('section')
- dom.innerText = this.getLabelByLanguage('placeholder')
- return dom
- }
- getLabelByLanguage(field, language) {
- let lang = language
- if (!lang) {
- const _lang = navigator.language
- lang = _lang.indexOf('zh') === 0 ? '' : 'en'
- }
- return this[`${field}${lang ? ('_' + lang) : '' }`]
- }
- }
- class HistoryPane extends Pane {
- localKey = 'swagger-toolkit-history'
- title = '浏览历史'
- title_en = 'History'
- placeholder = '暂无浏览历史数据'
- placeholder_en = 'No history at present'
- enableMarkBtn = true
- }
- class MarkPane extends Pane {
- localKey = 'swagger-toolkit-mark'
- title = '收藏夹'
- title_en = 'Favorites'
- placeholder = '暂无收藏数据, 点击 ⭐️ 按钮添加'
- placeholder_en = 'No favorite data, click ⭐️ button to add'
- afterGenerageDom() {
- this.dom
- }
- }
- class SideBar {
- static dom = null
- static panes = []
- static pathBtnGroupClassName = 'toolkit-path-btn-group'
- static copyInput = document.createElement('input')
- initCopyDOM() {
- SideBar.copyInput.classList.add('toolkit-hidden')
- document.body.appendChild(SideBar.copyInput)
- return this
- }
- addListeners() {
- window.addEventListener('hashchange', () => {
- const _path = location.hash.length > 0 ? location.hash.substr(1) : ''
- if (!_path) return
- const row = document.getElementById(_path) || (document.querySelector(`a[href="#${_path}"]`) && document.querySelector(`a[href="#${_path}"]`).closest('.opblock'))
- if (row) LinkStore.save(row, 'swagger-toolkit-history')
- this._updatePane('swagger-toolkit-history')
- })
- document.querySelector('#swagger-ui').addEventListener('mouseover', evt => {
- this._showPathBtnGroup(evt) // 显示在 path 栏中的按钮组
- })
- return this
- }
- _showPathBtnGroup(evt) {
- const opblock = evt.target.closest('.opblock')
- if (!opblock) return
- this._appendPathBtnGroupDOM(opblock)
- }
- _appendPathBtnGroupDOM(opblock) {
- if (opblock.querySelector('.' + SideBar.pathBtnGroupClassName)) return
- const group = document.createElement('div')
- const copyBtn = document.createElement('a')
- group.classList.add(SideBar.pathBtnGroupClassName)
- copyBtn.setAttribute('href', 'javascript:;')
- copyBtn.classList.add('btn-copy')
- copyBtn.innerText = '🔗'
- copyBtn.setAttribute('title', 'copy')
- group.appendChild(copyBtn)
- copyBtn.addEventListener('click', evt => {
- this._copyPath(evt)
- })
-
- const pathDOM = opblock.querySelector('.opblock-summary-path')
- if (pathDOM) pathDOM.appendChild(group)
- }
- _copyPath(evt) {
- evt.stopPropagation()
- const pathDOM = evt.target.closest('.opblock-summary-path')
- if (!pathDOM) return
- const pathLink = pathDOM.querySelector('a')
- if (!pathLink) return
- const path = pathLink.innerText
- SideBar.copyInput.value = path
- SideBar.copyInput.select()
- document.execCommand('Copy')
- console.log('copy successfuly')
- }
- generateDom() {
- const sidebar = document.createElement('sidebar')
- sidebar.id = 'swagger-toolkit-sidebar'
- SideBar.dom = sidebar
- return this
- }
- inject() {
- document.body.appendChild(SideBar.dom)
- return this
- }
- appendPanes() {
- for (const pane of SideBar.panes) {
- SideBar.dom.appendChild(pane.generateDom())
- }
- return this
- }
- _updatePane(key) {
- for (const pane of SideBar.panes) {
- if (pane.localKey !== key) continue
- pane.generateDom(true)
- }
- }
- appendPanesListeners() {
- SideBar.dom.addEventListener('click', evt => {
- if (evt.target.classList.contains('btn-delete')) {
- evt.preventDefault()
- evt.stopPropagation()
- const index = this._getRowIndex({ btnItem: evt.target })
- const key = evt.target.parentNode.parentNode.parentNode.getAttribute('data-key')
- LinkStore.remove(key, index)
- this._updatePane(key)
- } else if (evt.target.classList.contains('btn-mark')) {
- evt.preventDefault()
- evt.stopPropagation()
- const row = evt.target.parentNode.parentNode.getAttribute('data-row')
- LinkStore.add('swagger-toolkit-mark', JSON.parse(row), true)
- this._updatePane('swagger-toolkit-mark')
- }
- })
- }
- _getRowIndex({ btnItem }) {
- const listDom = Array.from(btnItem.parentNode.parentNode.parentNode.children)
- for (let index = listDom.length; index--;) {
- if (listDom[index] === btnItem.parentNode.parentNode) return index - 1
- }
- return -1
- }
- }
- Sheets.inject()
- SideBar.panes.push(new HistoryPane())
- SideBar.panes.push(new MarkPane())
- window.$$_SideBar = new SideBar()
- window.$$_SideBar
- .initCopyDOM()
- .addListeners()
- .generateDom()
- .appendPanes()
- .inject()
- .appendPanesListeners()
- })();