Swagger Toolkit

Swagger 站点工具脚本 💪 | 保存浏览历史 🕘 | 显示收藏夹 ⭐️ | 点击 path 快速定位 🎯

当前为 2020-04-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Swagger Toolkit
  3. // @namespace https://github.com/SublimeCT
  4. // @version 1.0.1
  5. // @description Swagger 站点工具脚本 💪 | 保存浏览历史 🕘 | 显示收藏夹 ⭐️ | 点击 path 快速定位 🎯
  6. // @note v1.0.1 增加当前页是不是 swagger 构建的文档判断; 自动展开所有 tag, 以定位到对应的 API;
  7. // @author Sven
  8. // @icon https://static1.smartbear.co/swagger/media/assets/swagger_fav.png
  9. // @match *://*/docs/index.html
  10. // @match *://*/docs/api/index.html
  11. // @match https://petstore.swagger.io
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. ; (() => {
  16. // @require file:///Users/test/projects/greasy_monkey_scripts/swagger_toolkit.js
  17. const TIMES = 30
  18. let current = 0
  19. let isLoaded = false
  20. const interval = setInterval(() => {
  21. if (++current >= TIMES) {
  22. clearInterval(interval)
  23. return
  24. }
  25. const item = document.querySelector('.opblock-tag')
  26. const swaggerAPI = window.SwaggerUIBundle
  27. if (!item || !swaggerAPI) return
  28. if (!isLoaded) {
  29. // 首先展开所有 tag, 否则无法定位
  30. const notOpenTags = document.querySelectorAll('.opblock-tag[data-is-open=false]') || []
  31. for (const tag of Array.from(notOpenTags)) {
  32. tag.click()
  33. }
  34. // 增加监听事件
  35. const wrapper = document.querySelector('.swagger-ui')
  36. wrapper.addEventListener('click', evt => {
  37. // 点击接口标题时在当前 URL 中加入锚点
  38. const linkTitleDom = evt.target.closest('.opblock-summary')
  39. if (linkTitleDom) {
  40. const linkDom = linkTitleDom.parentNode
  41. const isOpen = !linkDom.classList.contains('is-open')
  42. const hash = isOpen ? linkDom.id : ''
  43. if (hash) location.hash = hash
  44. return
  45. }
  46. // 点击接口中的 Model 时同步展开下方数据结构
  47. const modelLinkDom = evt.target.closest('ul.tab')
  48. if (modelLinkDom && evt.target.innerText.trim() === 'Model') {
  49. setTimeout(() => {
  50. const icons = modelLinkDom.nextElementSibling.querySelectorAll('.model-toggle.collapsed')
  51. if (icons.length) icons[icons.length - 1].click()
  52. }, 300)
  53. return
  54. }
  55. })
  56. if (location.hash) {
  57. observeHash()
  58. window.addEventListener('hashchange', observeHash)
  59. }
  60. isLoaded = true
  61. return
  62. }
  63. }, 300);
  64. const observeHash = evt => {
  65. const linkedDom = document.getElementById(location.hash.length > 0 ? location.hash.substr(1) : '')
  66. if (linkedDom) {
  67. const isOpen = linkedDom.classList.contains('is-open')
  68. linkedDom.scrollIntoView()
  69. if (!isOpen) linkedDom.querySelector('.opblock-summary').click()
  70. console.log('scroll into view: ', linkedDom, linkedDom.querySelector('.opblock-summary'))
  71. }
  72. }
  73. class Sheets {
  74. static sheets = `
  75. body {
  76. --row-width: 13vw;
  77. --row-min-width: 245px;
  78. --row-title-font-size: 14px;
  79. --body-wrapper-width: 80vw;
  80. --body-wrapper-margin-right: 3vw;
  81. --body-wrapper-min-width: 800px;
  82. --body-btn-group-width: 20px;
  83. }
  84.  
  85. /* 页面内容主体布局 */
  86. #swagger-ui div.topbar { display: flex; justify-content: flex-end; }
  87. #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) }
  88. #swagger-ui div.swagger-ui { display: flex; justify-content: flex-end; }
  89. #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) }
  90.  
  91. /* sidebar part */
  92. #swagger-toolkit-sidebar {
  93. width: var(--row-width);
  94. min-width: var(--row-min-width);
  95. display: flex;
  96. position: fixed;
  97. top: 0;
  98. left: 0;
  99. height: 100vh;
  100. flex-direction: column;
  101. justify-content: space-between;
  102. background-color: #FAFAFA;
  103. border-right: 1px solid #c4d6d6;
  104. }
  105. #swagger-toolkit-sidebar .list { width: 100%; }
  106. #swagger-toolkit-sidebar .list > header { font-size: 18px; background-color: #999; }
  107. #swagger-toolkit-sidebar .list > header > .title { color: #FFF; text-align: center; font-weight: 200; }
  108. #swagger-toolkit-sidebar .row { display: flex; padding-bottom: 5px; width: 100%; cursor: pointer; text-decoration: none; }
  109. #swagger-toolkit-sidebar .row.method-DELETE { background-color: rgba(249,62,62,.1); }
  110. #swagger-toolkit-sidebar .row.method-DELETE:hover { background-color: rgba(249,62,62,.5); }
  111. #swagger-toolkit-sidebar .row.method-GET { background-color: rgba(97,175,254,.1); }
  112. #swagger-toolkit-sidebar .row.method-GET:hover { background-color: rgba(97,175,254,.5); }
  113. #swagger-toolkit-sidebar .row.method-POST { background-color: rgba(73,204,144,.1); }
  114. #swagger-toolkit-sidebar .row.method-POST:hover { background-color: rgba(73,204,144,.5); }
  115. #swagger-toolkit-sidebar .row.method-PUT { background-color: rgba(252,161,48,.1); }
  116. #swagger-toolkit-sidebar .row.method-PUT:hover { background-color: rgba(252,161,48,.5); }
  117. #swagger-toolkit-sidebar .row.method-PATCH { background-color: rgba(80,227,194,.1); }
  118. #swagger-toolkit-sidebar .row.method-PATCH:hover { background-color: rgba(80,227,194,.5); }
  119.  
  120. #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)); }
  121. #swagger-toolkit-sidebar .row .method { display: flex; line-height: 45px; min-width: 64px; }
  122. #swagger-toolkit-sidebar .row .path > a { color: #409EFF; }
  123.  
  124. #swagger-toolkit-sidebar .row .btn-group { font-size: 12px; }
  125. #swagger-toolkit-sidebar .row .btn-group > a { text-decoration: none; display: block; }
  126. #swagger-toolkit-sidebar .row .btn-group > a:hover { font-size: 14px; }
  127.  
  128. /* helper */
  129. .tool-text-size-fixed { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  130. `
  131. static inject() {
  132. const sheet = document.createTextNode(Sheets.sheets)
  133. const el = document.createElement('style')
  134. el.id = 'swagger-toolkit-sheets'
  135. el.appendChild(sheet)
  136. document.getElementsByTagName('head')[0].appendChild(el)
  137. }
  138. }
  139. class LinkStore {
  140. key = ''
  141. path = ''
  142. method = ''
  143. description = '' // 接口名
  144. id = ''
  145. createdat = 0
  146. static MAX_LENGTH = 10
  147. static save(row, key) {
  148. const store = new LinkStore()
  149. store.id = row.id
  150. store.key = key
  151. store.method = row.querySelector('.opblock-summary-method').innerText
  152. store.path = row.querySelector('.opblock-summary-path').innerText
  153. store.description = row.querySelector('.opblock-summary-description').innerText
  154. LinkStore.add(key, store)
  155. }
  156. static add(key, store, filterRepeat) {
  157. let data = LinkStore.getStore(key)
  158. if (filterRepeat) {
  159. for (const row of data) {
  160. if (row.id === store.id && store.path === store.path) return false
  161. }
  162. }
  163. data.unshift(store)
  164. if (data.length > LinkStore.MAX_LENGTH) data = data.slice(0, LinkStore.MAX_LENGTH)
  165. localStorage.setItem(key, JSON.stringify(data))
  166. }
  167. static remove(key, index) {
  168. let data = LinkStore.getStore(key)
  169. data.splice(index, 1)
  170. localStorage.setItem(key, JSON.stringify(data))
  171. }
  172. static getStore(key) {
  173. let store = []
  174. try {
  175. const _store = localStorage.getItem(key)
  176. if (_store) store = JSON.parse(_store)
  177. } catch (err) {
  178. console.error(err)
  179. }
  180. return store
  181. }
  182. }
  183. class Pane {
  184. dom = null
  185. localKey = null
  186. title = null
  187. placeholder = '暂无数据'
  188. enableMarkBtn = false
  189. /**
  190. * 生成或更新当前 Pane
  191. * @description 将生成 `.list>(header>.title)+(a.row>(.method+.contents>(.description+a.path)))`
  192. */
  193. generateDom(isUpdate) {
  194. if (isUpdate) this.dom.innerHTML = ''
  195. const list = isUpdate ? this.dom : document.createElement('div')
  196. list.classList.add('list')
  197. list.classList.add(this.localKey)
  198. list.setAttribute('data-key', this.localKey)
  199. // 添加 header
  200. const header = document.createElement('header')
  201. const title = document.createElement('div')
  202. title.classList.add('title')
  203. title.innerText = this.title
  204. list.appendChild(header)
  205. header.appendChild(title)
  206. // 添加数据
  207. const data = LinkStore.getStore(this.localKey)
  208. for (const dataRow of data) {
  209. const row = document.createElement('a')
  210. row.href = '#' + dataRow.id
  211. row.setAttribute('data-row', JSON.stringify(dataRow))
  212. const method = document.createElement('div')
  213. method.innerText = dataRow.method
  214. const contents = document.createElement('div')
  215. const description = document.createElement('div')
  216. description.innerText = dataRow.description
  217. const path = document.createElement('div')
  218. const pathLink = document.createElement('a')
  219. pathLink.innerText = dataRow.path
  220. pathLink.href = '#' + dataRow.id
  221. const btnGroup = document.createElement('div')
  222. const markBtn = document.createElement('a')
  223. if (this.enableMarkBtn) {
  224. markBtn.href = 'javascript:;'
  225. markBtn.setAttribute('title', '收藏')
  226. markBtn.innerText = '⭐️'
  227. }
  228. const deleteBtn = document.createElement('a')
  229. deleteBtn.href = 'javascript:;'
  230. deleteBtn.setAttribute('title', '删除')
  231. deleteBtn.innerText = '✖️'
  232.  
  233. row.classList.add('row')
  234. row.classList.add('method-' + dataRow.method)
  235. method.classList.add('method')
  236. contents.classList.add('contents')
  237. description.classList.add('description')
  238. description.classList.add('tool-text-size-fixed')
  239. path.classList.add('path')
  240. btnGroup.classList.add('btn-group')
  241. if (this.enableMarkBtn) markBtn.classList.add('btn-mark')
  242. deleteBtn.classList.add('btn-delete')
  243.  
  244. path.appendChild(pathLink)
  245. contents.appendChild(description)
  246. contents.appendChild(path)
  247. // row.appendChild(method)
  248. row.appendChild(contents)
  249. row.appendChild(btnGroup)
  250. btnGroup.appendChild(deleteBtn)
  251. if (this.enableMarkBtn) btnGroup.appendChild(markBtn)
  252. list.appendChild(row)
  253. }
  254. if (data.length === 0) list.appendChild(this.getPlaceholderDom())
  255. this.dom = list
  256. if (typeof this.afterGenerageDom === 'function') this.afterGenerageDom()
  257. return list
  258. }
  259. getPlaceholderDom() {
  260. const dom = document.createElement('section')
  261. dom.innerText = this.placeholder
  262. return dom
  263. }
  264. }
  265. class HistoryPane extends Pane {
  266. localKey = 'swagger-toolkit-history'
  267. title = '浏览历史'
  268. placeholder = '暂无浏览历史数据'
  269. enableMarkBtn = true
  270. }
  271. class MarkPane extends Pane {
  272. localKey = 'swagger-toolkit-mark'
  273. title = '收藏夹'
  274. placeholder = '暂无收藏数据, 点击 ⭐️ 按钮添加'
  275. afterGenerageDom() {
  276. this.dom
  277. }
  278. }
  279. class SideBar {
  280. static dom = null
  281. static panes = []
  282. addListeners() {
  283. window.addEventListener('hashchange', () => {
  284. const _path = location.hash.length > 0 ? location.hash.substr(1) : ''
  285. if (!_path) return
  286. const row = document.getElementById(_path) || (document.querySelector(`a[href="#${_path}"]`) && document.querySelector(`a[href="#${_path}"]`).closest('.opblock'))
  287. if (row) LinkStore.save(row, 'swagger-toolkit-history')
  288. this._updatePane('swagger-toolkit-history')
  289. })
  290. return this
  291. }
  292. generateDom() {
  293. const sidebar = document.createElement('sidebar')
  294. sidebar.id = 'swagger-toolkit-sidebar'
  295. SideBar.dom = sidebar
  296. return this
  297. }
  298. inject() {
  299. document.body.appendChild(SideBar.dom)
  300. return this
  301. }
  302. appendPanes() {
  303. for (const pane of SideBar.panes) {
  304. SideBar.dom.appendChild(pane.generateDom())
  305. }
  306. return this
  307. }
  308. _updatePane(key) {
  309. for (const pane of SideBar.panes) {
  310. if (pane.localKey !== key) continue
  311. pane.generateDom(true)
  312. }
  313. }
  314. appendPanesListeners() {
  315. SideBar.dom.addEventListener('click', evt => {
  316. if (evt.target.classList.contains('btn-delete')) {
  317. evt.preventDefault()
  318. evt.stopPropagation()
  319. const index = this._getRowIndex({ btnItem: evt.target })
  320. const key = evt.target.parentNode.parentNode.parentNode.getAttribute('data-key')
  321. LinkStore.remove(key, index)
  322. this._updatePane(key)
  323. } else if (evt.target.classList.contains('btn-mark')) {
  324. evt.preventDefault()
  325. evt.stopPropagation()
  326. const row = evt.target.parentNode.parentNode.getAttribute('data-row')
  327. LinkStore.add('swagger-toolkit-mark', JSON.parse(row), true)
  328. this._updatePane('swagger-toolkit-mark')
  329. }
  330. })
  331. }
  332. _getRowIndex({ btnItem }) {
  333. const listDom = Array.from(btnItem.parentNode.parentNode.parentNode.children)
  334. for (let index = listDom.length; index--;) {
  335. if (listDom[index] === btnItem.parentNode.parentNode) return index - 1
  336. }
  337. return -1
  338. }
  339. }
  340. Sheets.inject()
  341. SideBar.panes.push(new HistoryPane())
  342. SideBar.panes.push(new MarkPane())
  343. window.$$_SideBar = new SideBar()
  344. window.$$_SideBar
  345. .addListeners()
  346. .generateDom()
  347. .appendPanes()
  348. .inject()
  349. .appendPanesListeners()
  350. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址