swagger-ui-2.7.0

swagger-ui 添加查找接口交互 此插件针对于2.0的swagger版本及以上,由SwaggerUI Search Supportting魔改而成.

  1. // ==UserScript==
  2. // @name swagger-ui-2.7.0
  3. // @namespace Violentmonkey Scripts
  4. // @match *://*/*/swagger-ui.html*
  5. // @match *://*/swagger-ui.html*
  6. // @grant none
  7. // @version 20230525
  8. // @description swagger-ui 添加查找接口交互 此插件针对于2.0的swagger版本及以上,由SwaggerUI Search Supportting魔改而成.
  9. // @run-at document-end
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. window.addEventListener('load', function () {
  14. /**
  15. * @file: EventEmitter
  16. * @author: 高国祥,王勤奋wajncn@gmail.com,baibaiwuchang
  17. * @date: 20220214
  18. * @description:
  19. */
  20. function assertType(type) {
  21. if (typeof type !== 'string') {
  22. throw new TypeError('type is not type of String!')
  23. }
  24. }
  25.  
  26. function assertFn(fn) {
  27. if (typeof fn !== 'function') {
  28. throw new TypeError('fn is not type of Function!')
  29. }
  30. }
  31.  
  32. function EventEmitter() {
  33. this._events = {}
  34. }
  35.  
  36. function on(type, fn) {
  37. assertType(type)
  38. assertFn(fn)
  39. this._events[type] = this._events[type] || []
  40. this._events[type].push({
  41. type: 'always',
  42. fn: fn
  43. })
  44. }
  45.  
  46. function prepend(type, fn) {
  47. assertType(type)
  48. assertFn(fn)
  49. this._events[type] = this._events[type] || []
  50. this._events[type].unshift({
  51. type: 'always',
  52. fn: fn
  53. })
  54. }
  55.  
  56. function prependOnce(type, fn) {
  57. assertType(type)
  58. assertFn(fn)
  59. this._events[type] = this._events[type] || []
  60. this._events[type].unshift({
  61. type: 'once',
  62. fn: fn
  63. })
  64. }
  65.  
  66. function once(type, fn) {
  67. assertType(type)
  68. assertFn(fn)
  69. this._events[type] = this._events[type] || []
  70. this._events[type].push({
  71. type: 'once',
  72. fn: fn
  73. })
  74. }
  75.  
  76. function off(type, nullOrFn) {
  77. assertType(type)
  78. if (!this._events[type]) return
  79. if (typeof nullOrFn === 'function') {
  80. var index = this._events[type].findIndex(function (event) {
  81. return event.fn === nullOrFn
  82. })
  83. if (index >= 0) {
  84. this._events[type].splice(index, 1)
  85. }
  86. } else {
  87. delete this._events[type]
  88. }
  89. }
  90.  
  91. function emit(type /*, arguments */) {
  92. assertType(type)
  93. var args = [].slice.call(arguments, 1)
  94. var self = this
  95. if (this._events[type]) {
  96. this._events[type].forEach(function (event) {
  97. event.fn.apply(null, args)
  98. if (event.type === 'once') {
  99. self.off(type, event.fn)
  100. }
  101. })
  102. }
  103. }
  104.  
  105. EventEmitter.prototype.on = EventEmitter.prototype.addListener = on
  106. EventEmitter.prototype.once = EventEmitter.prototype.addOnceListener = once
  107. EventEmitter.prototype.prepend = EventEmitter.prototype.prependListener = prepend
  108. EventEmitter.prototype.prependOnce = EventEmitter.prototype.prependOnceListener = prependOnce
  109. EventEmitter.prototype.off = EventEmitter.prototype.removeListener = off
  110. EventEmitter.prototype.emit = EventEmitter.prototype.trigger = emit
  111.  
  112. if (typeof module !== 'undefined') {
  113. module.exports = EventEmitter
  114. }
  115.  
  116.  
  117. function KeyExtra(opt) {
  118. this._init(opt)
  119. }
  120.  
  121. KeyExtra.prototype = new EventEmitter()
  122. KeyExtra.prototype.constructor = KeyExtra
  123.  
  124. KeyExtra.prototype._init = function (opt) {
  125. var keyExtra = this
  126.  
  127. // double key press
  128. var doublePressTimeoutMs = 600
  129. var lastKeypressTime = 0
  130. var lastKeyChar = null
  131.  
  132. function doubleHandle(type) {
  133. return function (evt) {
  134. var thisCharCode = evt.key.toUpperCase()
  135. if (lastKeyChar === null) {
  136. lastKeyChar = thisCharCode
  137. lastKeypressTime = new Date()
  138. return
  139. }
  140. if (thisCharCode === lastKeyChar) {
  141. var thisKeypressTime = new Date()
  142. if (thisKeypressTime - lastKeypressTime <= doublePressTimeoutMs) {
  143. keyExtra.emit('double-' + type, thisCharCode)
  144. }
  145. }
  146. lastKeyChar = null
  147. lastKeypressTime = 0
  148. }
  149. }
  150.  
  151. document && document.addEventListener('keypress', doubleHandle('keypress'))
  152. document && document.addEventListener('keydown', doubleHandle('keydown'))
  153. }
  154.  
  155.  
  156. setTimeout(
  157. function () {
  158. (
  159. function ($) {
  160. var swaggerVersion = 1
  161. if (typeof SwaggerUIBundle === 'function') {
  162. // swagger-ui v2-v3
  163. swaggerVersion = 2
  164. var script = document.createElement('script')
  165. script.src = '//cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js'
  166. script.onload = function (ev) {
  167. registerSearchUI()
  168. }
  169. document.head.appendChild(script)
  170. return
  171. }
  172.  
  173. if (typeof window.swaggerUi === 'undefined') {
  174. console.error('window.swaggerUi is not defined, so we consider that the page isn\'t swagger-ui.')
  175. return
  176. }
  177. if (typeof $ === 'undefined') {
  178. console.error('jQuery is not found, so we consider that the page isn\'t swagger-ui.')
  179. return
  180. }
  181. registerSearchUI()
  182.  
  183.  
  184. function registerSearchUI() {
  185. var $ = window.jQuery
  186. var dom = $('<div style="margin-top: 15px;"></div>')
  187. dom.attr('class', 'inject-dom-container')
  188.  
  189. var btns = $('<div></div>')
  190. btns.attr('class', 'inject-btn-container')
  191.  
  192. function listAll() {
  193. $('.collapseResource').click()
  194. }
  195.  
  196. function hideAll() {
  197. $('.endpoints').css({ display: 'none' })
  198. }
  199.  
  200. function expendAll() {
  201. $('.expandResource').click()
  202. }
  203.  
  204. swaggerVersion === 1 && btns.append(
  205. $('<button>List All</button>').on('click', listAll),
  206. $('<button>Hide All</button>').on('click', hideAll),
  207. $('<button>Expend All</button>').on('click', expendAll),
  208. )
  209.  
  210. swaggerVersion === 1 && dom.append(btns)
  211. dom.append([
  212. '<div style="text-align: center;">',
  213. '<br/>',
  214. '<small style="margin-bottom: 10px">双击或长按Shift搜索 Esc关闭</small>',
  215. '</div>',
  216. '<div class="search-container" style="display: none;">',
  217. '<div class="search-main">',
  218. '<input class="search-input"/>',
  219. '<ul class="search-found-list">',
  220. '</ul>',
  221. '</div>',
  222. '</div>'
  223. ].join(''))
  224.  
  225. var searchContainer = dom.find('.search-container')
  226. new KeyExtra()
  227. .on('double-keydown', function (charCode) {
  228. if (charCode === 'A' || charCode === 'SHIFT') {
  229. setTimeout(function () {
  230. $('body').css({ overflow: 'hidden' })
  231. searchContainer.show()
  232. searchContainer.find('.search-input').focus().select()
  233. }, 0)
  234. }
  235. })
  236.  
  237. function hideSearch() {
  238. $('body').css({ overflow: '' })
  239. searchContainer.hide()
  240. }
  241.  
  242. document.addEventListener('keydown', function (evt) {
  243. if (evt.key === 'Escape') {
  244. hideSearch()
  245. }
  246. })
  247.  
  248. var COUNT = 20
  249.  
  250. function search(val) {
  251. val = typeof val !== 'string' ? '' : val.trim()
  252.  
  253. if (!val) {
  254. foundListDom.empty()
  255. return
  256. }
  257.  
  258. var type = ''
  259. if (/^(p|s|m): ([^]+)$/.test(val)) {
  260. type = RegExp.$1
  261. val = RegExp.$2
  262. }
  263.  
  264. var keywords = val.split(/[+ ]/)
  265. var foundList = []
  266.  
  267. list.some(function (entity) {
  268. if (foundList.length === 30) {
  269. return true
  270. }
  271. var matched_types = []
  272. var matched = keywords.every(function (keyword) {
  273. function find(type, keyword) {
  274. // console.log(entity);
  275. if (entity[type].toLowerCase().includes(keyword.toLowerCase())) {
  276. if (!matched_types.includes(type)) {
  277. matched_types.push(type)
  278. }
  279. return true
  280. }
  281. }
  282.  
  283. if (type) {
  284. return find(type, keyword)
  285. }
  286. else {
  287. return ['p', 's', 'm'].some(function (type) {
  288. return find(type, keyword)
  289. })
  290. }
  291. })
  292.  
  293. if (matched) {
  294. foundList.push({
  295. type: matched_types.join(' '),
  296. entity: entity
  297. })
  298. }
  299. })
  300.  
  301. foundListDom.empty()
  302.  
  303. function item(data, i) {
  304. var html = '<li class="search-item ' + (
  305. i === 0 ? 'active' : ''
  306. ) + '">'
  307. + '<span class="search-item-type">' + data.type + '</span>'
  308. + ': '
  309. + '<span class="search-item-method">' + data.entity.m.toUpperCase() + '</span>'
  310. + ' '
  311. + '<span class="search-item-path">' + data.entity.p + '</span>'
  312. + '<span class="search-item-summary">' + data.entity.s + '</span>'
  313. + '<button class="copy-route-btn" title="复制路由" onclick="navigator.clipboard.writeText(\'' + data.entity.p + '\').then(() => alert(\'路由已复制到剪贴板\'))">📋</button>'
  314. + '</li>'
  315.  
  316. return $(html).on('click', function () {
  317. console.log('click', data)
  318. var path = (swaggerVersion === 1 ? data.entity.url : data.entity.url.slice(1))
  319. var href = '#' + path
  320. if (swaggerVersion === 1) {
  321. var link = $('.toggleOperation[href=' + JSON.stringify(href) + ']')
  322. link.parents('ul.endpoints').css({ display: 'block' })
  323. link[0].scrollIntoView()
  324. var operation = link.parents('.operation')
  325. var content = operation.find('.content')
  326. content.css('display') === 'none' && link[0].click()
  327. }
  328. else {
  329. // swagger 中文版本
  330. var tag = data.entity.methodEntity.tags[0]
  331. tag = tag.replaceAll(")","\\)").replaceAll("(","\\(").replaceAll(" ","_")
  332. var tagDOM = $('#operations-tag-' + tag)
  333. if (!tagDOM.parent().hasClass('is-open')) {
  334. tagDOM.click()
  335. }
  336. path = path.replaceAll(")","\\)").replaceAll("(","\\(").replaceAll(" ","\\ ")
  337. path = path.replaceAll(/\//g, '-')
  338. var toggleDOM = $('#operations' + path)
  339. if (!toggleDOM.hasClass('is-open')) {
  340. toggleDOM.children().eq(0).click()
  341. }
  342. toggleDOM[0].scrollIntoView()
  343. }
  344. hideSearch()
  345. foundListDom.empty()
  346. })
  347. }
  348.  
  349. if (!foundList.length) {
  350. foundListDom.append(
  351. '<li class="search-item">' + 'Not Found :(' + '</li>'
  352. )
  353. }
  354. else {
  355. foundListDom.append(
  356. foundList.map(item)
  357. )
  358.  
  359. var sumHeight = 1
  360. var over = Array.from(foundListDom.children('.search-item')).some(function (dom, i) {
  361. if (i === COUNT) {
  362. return true
  363. }
  364. sumHeight += $(dom).prop('clientHeight') + 1
  365. })
  366. over && foundListDom.css({ 'max-height': sumHeight + 'px' })
  367. }
  368. }
  369.  
  370. var foundListDom = dom.find('.search-found-list')
  371. dom.find('.search-input')
  372. .on('input', function (evt) {
  373. search(evt.target.value)
  374. })
  375. .on('focus', function (evt) {
  376. search(evt.target.value)
  377. })
  378. // .on('blur', function (evt) { setTimeout(function () {foundListDom.empty()}, 300) })
  379. .on('keydown', function (evt) {
  380. var activeIndex = null
  381. var listDoms = foundListDom.find('.search-item')
  382.  
  383. function findActive() {
  384. Array.from(listDoms).some(function (dom, i) {
  385. if ($(dom).hasClass('active')) {
  386. $(dom).removeClass('active')
  387. activeIndex = i
  388. }
  389. })
  390. }
  391.  
  392. var crlKey = evt.metaKey || evt.ctrlKey
  393. var offset = crlKey ? COUNT : 1
  394. var isUp = null
  395. var prevIndex = activeIndex
  396. switch (evt.keyCode) {
  397. case 38: // UP
  398. findActive()
  399. activeIndex = (
  400. listDoms.length + activeIndex - offset
  401. ) % listDoms.length
  402. listDoms.eq(activeIndex).addClass('active')
  403. isUp = true
  404. break
  405. case 40: // DOWN
  406. findActive()
  407. activeIndex = (
  408. activeIndex + offset
  409. ) % listDoms.length
  410. listDoms.eq(activeIndex).addClass('active')
  411. isUp = false
  412. break
  413. case 13: // ENTER
  414. findActive()
  415. listDoms[activeIndex] && listDoms[activeIndex].click()
  416. return
  417. }
  418. if (isUp === null) {
  419. return
  420. }
  421. evt.preventDefault()
  422. var rang = [
  423. foundListDom.prop('scrollTop'),
  424. foundListDom.prop('scrollTop') + foundListDom.prop('clientHeight') - 10
  425. ]
  426. // console.log(rang, listDoms[activeIndex].offsetTop)
  427. // console.dir(foundListDom[0])
  428. // console.log('!', listDoms[activeIndex].offsetTop, rang);
  429. if (listDoms[activeIndex]) {
  430. if (!(
  431. listDoms[activeIndex].offsetTop >= rang[0] && listDoms[activeIndex].offsetTop <= rang[1]
  432. )) {
  433. // debugger;
  434. if (activeIndex === 0) {
  435. foundListDom[0].scrollTop = 0
  436. } else if (activeIndex === listDoms.length - 1) {
  437. foundListDom[0].scrollTop = foundListDom.prop('scrollHeight')
  438. } else {
  439. foundListDom[0].scrollTop +=
  440. isUp ? -foundListDom.prop('clientHeight') : foundListDom.prop('clientHeight')
  441. }
  442. }
  443. }
  444.  
  445. //console.dir(foundListDom[0])
  446. //console.dir(listDoms[activeIndex]);
  447. })
  448.  
  449. var list = []
  450. var url
  451. if (swaggerVersion === 1) {
  452. url = window.swaggerUi.api && window.swaggerUi.api.url
  453. } else {
  454. url = $('.download-url-input').val()
  455. // global ui variable
  456. if (!url && typeof window.ui !== 'undefined') {
  457. var config = window.ui.getConfigs()
  458. url = config.url || (config.urls[0] && config.urls[0].url)
  459. }
  460. }
  461.  
  462. if (url) {
  463.  
  464. function analysisData(data) {
  465. console.log('data', data)
  466. $.each(data.paths, function (path, methodSet) {
  467. $.each(methodSet, function (method, methodEntity) {
  468. // @todo:: array ??
  469. methodEntity.tags.join(',')
  470. methodEntity.operationId
  471. methodEntity.summary
  472. var u = "";
  473. // url非中文的版本
  474. if(swaggerVersion==1){
  475. u = '!/' + methodEntity.tags.join(',').replace(/[^a-zA-Z\d]/g, function(str) { return str.charCodeAt(0); }) + '/' + methodEntity.operationId
  476. }else{
  477. // url是中文的版本
  478. u = '!/' + methodEntity.tags.join(',') + '/' + methodEntity.operationId
  479. }
  480.  
  481. list.push({
  482. methodEntity: methodEntity,
  483. url: u,
  484. s: methodEntity.summary,
  485. m: method,
  486. p: path
  487. })
  488. })
  489. })
  490.  
  491. console.log('list', list)
  492. dom.insertAfter( swaggerVersion === 1 ? $('#header') : $('.topbar'))
  493.  
  494.  
  495. if(swaggerVersion !=1){
  496. // url hash为: #/(内部)债券基础信息接口/listBondRemainingTenorDTOUsingPOST_1
  497. var urlHash = decodeURIComponent(window.location.hash);
  498. if(urlHash != null && urlHash.length > 2) {
  499. // 去掉第一个#
  500. urlHash = urlHash.slice(1);
  501. var urlHashArrays = urlHash.split("/");
  502. var tag = urlHashArrays[1];
  503. tag = tag.replaceAll(")","\\)").replaceAll("(","\\(").replaceAll(" ","_");
  504. var tagDOM = $('#operations-tag-' + tag);
  505. if (!tagDOM.parent().hasClass('is-open')) {
  506. tagDOM.click();
  507. }
  508.  
  509. if(urlHashArrays.length == 3){
  510. path = urlHash.replaceAll(")","\\)").replaceAll("(","\\(").replaceAll(" ","\\ ");
  511. path = path.replaceAll(/\//g, '-');
  512. var toggleDOM = $('#operations' + path)
  513. if (!toggleDOM.hasClass('is-open')) {
  514. toggleDOM.children().eq(0).click();
  515. }
  516. toggleDOM[0].scrollIntoView();
  517. }else{
  518. tagDOM[0].scrollIntoView();
  519. }
  520. }
  521. }
  522.  
  523. }
  524.  
  525. $.ajax({
  526. url: url,
  527. dataType: 'text',
  528. success: function (data) {
  529. if (/^\s*[{[]/.test(data)) {
  530. // json string is error
  531. data = eval('x = ' + data + '\n x;')
  532. analysisData(data)
  533. } else {
  534. // yaml text
  535. var script = document.createElement('script')
  536. script.src = '//cdn.bootcss.com/js-yaml/3.10.0/js-yaml.min.js'
  537. document.head.appendChild(script)
  538. script.onload = function () {
  539. data = jsyaml.safeLoad(data)
  540. analysisData(data)
  541. }
  542. }
  543. }
  544. })
  545. }
  546.  
  547. $('head').append(
  548. '<style type="text/css">'
  549. + '.inject-btn-container {'
  550. + 'text-align: center;'
  551. + '}'
  552. + '.inject-btn-container button {'
  553. + 'margin-left: 5px;'
  554. + 'margin-right: 5px;'
  555. + '}'
  556. + '.search-item-type{'
  557. + 'display: inline-block;'
  558. + 'min-width: 15px;'
  559. + '}'
  560. + '.search-item-method {'
  561. + 'display: inline-block;'
  562. + 'width: 65px;'
  563. + 'text-align: center'
  564. + '}'
  565. + '.search-item-summary {'
  566. + 'display: inline-block;'
  567. + 'width: auto;'
  568. + 'float: right;'
  569. + 'max-width: 200px;'
  570. + 'overflow: hidden;'
  571. + 'text-overflow: ellipsis;'
  572. + 'white-space: nowrap;'
  573. + 'text-align: right;'
  574. + '}'
  575. + '.search-main {'
  576. + 'position: static;'
  577. + 'margin: 40px auto 40px;'
  578. + 'width: 68%;'
  579. + 'min-width: 500px;'
  580. + '}'
  581. + '.search-container {'
  582. + 'overflow-y: auto;'
  583. + 'background-color: rgba(0, 0, 0, .3);'
  584. + 'position: fixed;'
  585. + 'left: 0;'
  586. + 'right: 0;'
  587. + 'top: 0;'
  588. + 'bottom: 0;'
  589. + 'z-index: 1000;'
  590. + '}'
  591. + '.search-input {'
  592. + 'line-height: 30px;'
  593. + 'font-size: 18px;'
  594. + 'display: block;'
  595. + 'margin: auto;'
  596. + 'width: 100%;'
  597. + 'border: none;'
  598. + 'border-bottom: 1px solid #89bf04;'
  599. + 'padding: 4px 10px 2px;'
  600. + 'box-sizing: border-box;'
  601. + 'height: 41px;'
  602. + 'border: 1px solid #ccc;'
  603. + 'border-color: #66B029;'
  604. + '}'
  605. + '.search-input:focus {'
  606. + 'outline: none;'
  607. + '}'
  608. + '.search-found-list {'
  609. + 'position: static;'
  610. + 'left: 0;'
  611. + 'right: 0;'
  612. + 'padding: 0;'
  613. // + 'max-height: 200px;'
  614. + 'overflow: auto;'
  615. + ''
  616. + '}'
  617. + '.search-found-list {'
  618. + 'margin-top: 2px;'
  619. + 'list-style: none;'
  620. + '}'
  621. + '.search-item.active, .search-item:hover {'
  622. + 'background-color: #eee;'
  623. + '}'
  624. + '.search-item {'
  625. + 'cursor: pointer;'
  626. + 'background-color: #fff;'
  627. + 'padding: 7px 15px;'
  628. + 'border: 1px solid #333;'
  629. + 'border-bottom: none;'
  630. + '}'
  631. + '.search-item:last-child {'
  632. + 'border-bottom: 1px solid #333;'
  633. + '}'
  634. + '.copy-route-btn {'
  635. + 'float: right;'
  636. + 'margin-left: 10px;'
  637. + 'background: none;'
  638. + 'border: none;'
  639. + 'cursor: pointer;'
  640. + 'font-size: 14px;'
  641. + 'color: #666;'
  642. + '}'
  643. + '.copy-route-btn:hover {'
  644. + 'color: #333;'
  645. + '}'
  646. + '</style>'
  647. )
  648.  
  649. // auto scrollIntoView by hash
  650. setTimeout(function () {
  651. var a = $('a[href="' + location.hash + '"]')[0]
  652. a && a.scrollIntoView()
  653. }, 200)
  654.  
  655. }
  656.  
  657.  
  658. }
  659. )(window.jQuery)
  660. },
  661. 1000
  662. )
  663. })