UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

1,084 lines (991 loc) 41.2 kB
import { css, LitElement, unsafeCSS } from 'lit'; import { marked } from 'marked'; import Prism from 'prismjs'; import 'prismjs/components/prism-css'; import 'prismjs/components/prism-yaml'; import 'prismjs/components/prism-go'; import 'prismjs/components/prism-java'; import 'prismjs/components/prism-json'; import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-python'; import 'prismjs/components/prism-http'; import 'prismjs/components/prism-csharp'; // Styles import FontStyles from '~/styles/font-styles'; import InputStyles from '~/styles/input-styles'; import FlexStyles from '~/styles/flex-styles'; import TableStyles from '~/styles/table-styles'; import EndpointStyles from '~/styles/endpoint-styles'; import PrismStyles from '~/styles/prism-styles'; import TabStyles from '~/styles/tab-styles'; import NavStyles from '~/styles/nav-styles'; import InfoStyles from '~/styles/info-styles'; import CustomStyles from '~/styles/custom-styles'; // import { expandCollapseNavBarTag } from '@/templates/navbar-template'; import { advancedSearch, getMatchedPaths, getMatchedComponents, rapidocApiKey, sleep } from '~/utils/common-utils'; import ProcessSpec from '~/utils/spec-parser'; import mainBodyTemplate from '~/templates/main-body-template'; import { applyApiKey, onClearAllApiKeys } from '~/templates/security-scheme-template'; import { setApiServer } from '~/templates/server-template'; export default class RapiDoc extends LitElement { constructor() { super(); const intersectionObserverOptions = { root: this.getRootNode().host, rootMargin: '-50px 0px -50px 0px', // when the element is visible 100px from bottom threshold: 0, }; this.showSummaryWhenCollapsed = true; // Will activate intersection observer only after spec load and hash analyze // to scroll to the proper element without being reverted by observer behavior this.isIntersectionObserverActive = false; this.intersectionObserver = new IntersectionObserver((entries) => { this.onIntersect(entries); }, intersectionObserverOptions); } static get properties() { return { // Heading headingText: { type: String, attribute: 'heading-text' }, gotoPath: { type: String, attribute: 'goto-path' }, // Spec updateRoute: { type: String, attribute: 'update-route' }, routePrefix: { type: String, attribute: 'route-prefix' }, specUrl: { type: String, attribute: 'spec-url' }, sortTags: { type: String, attribute: 'sort-tags' }, sortSchemas: { type: String, attribute: 'sort-schemas' }, generateMissingTags: { type: String, attribute: 'generate-missing-tags' }, sortEndpointsBy: { type: String, attribute: 'sort-endpoints-by' }, specFile: { type: String, attribute: false }, // UI Layouts layout: { type: String }, renderStyle: { type: String, attribute: 'render-style' }, defaultSchemaTab: { type: String, attribute: 'default-schema-tab' }, responseAreaHeight: { type: String, attribute: 'response-area-height' }, fillRequestFieldsWithExample: { type: String, attribute: 'fill-request-fields-with-example' }, persistAuth: { type: String, attribute: 'persist-auth' }, onNavTagClick: { type: String, attribute: 'on-nav-tag-click' }, // Schema Styles schemaStyle: { type: String, attribute: 'schema-style' }, schemaExpandLevel: { type: Number, attribute: 'schema-expand-level' }, schemaDescriptionExpanded: { type: String, attribute: 'schema-description-expanded' }, schemaHideReadOnly: { type: String, attribute: 'schema-hide-read-only' }, schemaHideWriteOnly: { type: String, attribute: 'schema-hide-write-only' }, // API Server apiKeyName: { type: String, attribute: 'api-key-name' }, apiKeyLocation: { type: String, attribute: 'api-key-location' }, apiKeyValue: { type: String, attribute: 'api-key-value' }, defaultApiServerUrl: { type: String, attribute: 'default-api-server' }, serverUrl: { type: String, attribute: 'server-url' }, oauthReceiver: { type: String, attribute: 'oauth-receiver' }, // Hide/Show Sections & Enable Disable actions showHeader: { type: String, attribute: 'show-header' }, showSideNav: { type: String, attribute: 'show-side-nav' }, showInfo: { type: String, attribute: 'show-info' }, allowAuthentication: { type: String, attribute: 'allow-authentication' }, allowTry: { type: String, attribute: 'allow-try' }, showCurlBeforeTry: { type: String, attribute: 'show-curl-before-try' }, allowSpecUrlLoad: { type: String, attribute: 'allow-spec-url-load' }, allowSpecFileLoad: { type: String, attribute: 'allow-spec-file-load' }, allowSpecFileDownload: { type: String, attribute: 'allow-spec-file-download' }, allowSearch: { type: String, attribute: 'allow-search' }, allowAdvancedSearch: { type: String, attribute: 'allow-advanced-search' }, allowServerSelection: { type: String, attribute: 'allow-server-selection' }, allowSchemaDescriptionExpandToggle: { type: String, attribute: 'allow-schema-description-expand-toggle' }, showComponents: { type: String, attribute: 'show-components' }, pageDirection: { type: String, attribute: 'page-direction' }, scrollBehavior: { type: String, attribute: 'scroll-behavior' }, // Main Colors and Font theme: { type: String }, bgColor: { type: String, attribute: 'bg-color' }, textColor: { type: String, attribute: 'text-color' }, headerColor: { type: String, attribute: 'header-color' }, primaryColor: { type: String, attribute: 'primary-color' }, fontSize: { type: String, attribute: 'font-size' }, regularFont: { type: String, attribute: 'regular-font' }, monoFont: { type: String, attribute: 'mono-font' }, loadFonts: { type: String, attribute: 'load-fonts' }, cssFile: { type: String, attribute: 'css-file' }, cssClasses: { type: String, attribute: 'css-classes' }, // Nav Bar Colors navBgColor: { type: String, attribute: 'nav-bg-color' }, navTextColor: { type: String, attribute: 'nav-text-color' }, navHoverBgColor: { type: String, attribute: 'nav-hover-bg-color' }, navHoverTextColor: { type: String, attribute: 'nav-hover-text-color' }, navAccentColor: { type: String, attribute: 'nav-accent-color' }, navAccentTextColor: { type: String, attribute: 'nav-accent-text-color' }, navActiveItemMarker: { type: String, attribute: 'nav-active-item-marker' }, navItemSpacing: { type: String, attribute: 'nav-item-spacing' }, showMethodInNavBar: { type: String, attribute: 'show-method-in-nav-bar' }, usePathInNavBar: { type: String, attribute: 'use-path-in-nav-bar' }, infoDescriptionHeadingsInNavBar: { type: String, attribute: 'info-description-headings-in-navbar' }, // Fetch Options fetchCredentials: { type: String, attribute: 'fetch-credentials' }, // Filters matchPaths: { type: String, attribute: 'match-paths' }, matchType: { type: String, attribute: 'match-type' }, removeEndpointsWithBadgeLabelAs: { type: String, attribute: 'remove-endpoints-with-badge-label-as' }, // Internal Properties loading: { type: Boolean }, // indicates spec is being loaded focusedElementId: { type: String }, // updating the focusedElementId will automatically render appropriate section in focused mode showAdvancedSearchDialog: { type: Boolean }, advancedSearchMatches: { type: Object }, searchVal: { type: String }, }; } static get styles() { return [ FontStyles, InputStyles, FlexStyles, TableStyles, EndpointStyles, PrismStyles, TabStyles, NavStyles, InfoStyles, css` :host { all: initial; display:flex; flex-direction: column; min-width:360px; width:100%; height:100%; margin:0; padding:0; overflow: hidden; letter-spacing:normal; color:var(--fg); background-color:var(--bg); font-family:var(--font-regular); container-type: inline-size; } :where(button, input[type="checkbox"], [tabindex="0"]):focus-visible { box-shadow: var(--focus-shadow); } :where(input[type="text"], input[type="password"], select, textarea):focus-visible { border-color: var(--primary-color); } .body { display:flex; height:100%; width:100%; overflow:hidden; } .main-content { margin:0; padding: 0; display:block; flex:1; height:100%; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: var(--border-color) transparent; } .main-content-inner--view-mode { padding: 0 8px; } .main-content::-webkit-scrollbar { width: 8px; height: 8px; } .main-content::-webkit-scrollbar-track { background:transparent; } .main-content::-webkit-scrollbar-thumb { background-color: var(--border-color); } .section-gap.section-tag { border-bottom:1px solid var(--border-color); } .section-gap, .section-gap--focused-mode, .section-gap--read-mode { padding: 0px 4px; } .section-tag-header { position:relative; cursor: n-resize; padding: 12px 0; } .collapsed .section-tag-header:hover { cursor: s-resize; } .section-tag-header:hover { background-image: linear-gradient(to right, rgba(0,0,0,0), var(--border-color), rgba(0,0,0,0)); } .section-tag-header:hover::after { position:absolute; margin-left:-24px; font-size:20px; top: calc(50% - 14px); color:var(--primary-color); content: '⬆'; } .collapsed .section-tag-header::after { position:absolute; margin-left:-24px; font-size:20px; top: calc(50% - 14px); color: var(--border-color); content: '⬇'; } .collapsed .section-tag-header:hover::after { color:var(--primary-color); } .collapsed .section-tag-body { display:none; } .logo { height:36px; width:36px; margin-left:5px; } .only-large-screen-flex, .only-large-screen { display:none; } .tag.title { text-transform: uppercase; } .main-header { background-color:var(--header-bg); color:var(--header-fg); width:100%; } .header-title { font-size:calc(var(--font-size-regular) + 8px); padding:0 8px; } input.header-input{ background:var(--header-color-darker); color:var(--header-fg); border:1px solid var(--header-color-border); flex:1; padding-right:24px; border-radius:3px; } input.header-input::placeholder { opacity:0.4; } .loader { margin: 16px auto 16px auto; border: 4px solid var(--bg3); border-radius: 50%; border-top: 4px solid var(--primary-color); width: 36px; height: 36px; animation: spin 2s linear infinite; } .expanded-endpoint-body { position: relative; padding: 6px 0px; } .expanded-endpoint-body .tag-description { background: var(--code-bg); border-radius: var(--border-radius); transition: max-height .2s ease-out; } .expanded-endpoint-body .tag-icon { transition: transform .2s ease-out; } .expanded-endpoint-body .tag-icon.expanded { transform: rotate(180deg); } .divider { border-top: 2px solid var(--border-color); margin: 24px 0; width:100%; } .tooltip { cursor:pointer; border: 1px solid var(--border-color); border-left-width: 4px; margin-left:2px; } .tooltip a { color: var(--fg2); text-decoration: none; } .tooltip-text { color: var(--fg2); max-width: 400px; position: absolute; z-index:1; background-color: var(--bg2); visibility: hidden; overflow-wrap: break-word; } .tooltip:hover { color: var(--primary-color); border-color: var(--primary-color); } .tooltip:hover a:hover { color: var(--primary-color); } .tooltip:hover .tooltip-text { visibility: visible; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .nav-method { font-weight: bold; margin-right: 4px; font-size: calc(var(--font-size-small) - 2px); white-space: nowrap; } .nav-method.false { display: none; } .nav-method.as-colored-text.get { color:var(--nav-get-color); } .nav-method.as-colored-text.put { color:var(--nav-put-color); } .nav-method.as-colored-text.post { color:var(--nav-post-color); } .nav-method.as-colored-text.delete { color:var(--nav-delete-color); } .nav-method.as-colored-text.head, .nav-method.as-colored-text.patch, .nav-method.as-colored-text.options { color:var(--nav-head-color); } .nav-method.as-colored-block { padding: 1px 4px; min-width: 30px; border-radius: 4px 0 0 4px; color: #000; } .colored-block .nav-method.as-colored-block { outline: 1px solid; } .nav-method.as-colored-block.get { background-color: var(--blue); } .nav-method.as-colored-block.put { background-color: var(--orange); } .nav-method.as-colored-block.post { background-color: var(--green); } .nav-method.as-colored-block.delete { background-color: var(--red); } .nav-method.as-colored-block.head, .nav-method.as-colored-block.patch , .nav-method.as-colored-block.options { background-color: var(--yellow); } @container (min-width: 768px) { .nav-bar { width: 260px; display:flex; } .only-large-screen { display:block; } .only-large-screen-flex { display:flex; } .section-gap { padding: 0 0 0 24px; } .section-gap--focused-mode { padding: 24px 8px; } .section-gap--read-mode { padding: 24px 8px; } .endpoint-body { position: relative; padding:36px 0 48px 0; } } @container (min-width: 1024px) { .nav-bar { width: ${unsafeCSS(this.fontSize === 'default' ? '300px' : this.fontSize === 'large' ? '315px' : '330px')}; display:flex; } .section-gap--focused-mode { padding: 12px 80px 12px 80px; } .section-gap--read-mode { padding: 24px 80px 12px 80px; } }`, CustomStyles, ]; } // Startup connectedCallback() { super.connectedCallback(); const parent = this.parentElement; if (parent) { if (parent.offsetWidth === 0 && parent.style.width === '') { parent.style.width = '100vw'; } if (parent.offsetHeight === 0 && parent.style.height === '') { parent.style.height = '100vh'; } if (parent.tagName === 'BODY') { if (!parent.style.marginTop) { parent.style.marginTop = '0'; } if (!parent.style.marginRight) { parent.style.marginRight = '0'; } if (!parent.style.marginBottom) { parent.style.marginBottom = '0'; } if (!parent.style.marginLeft) { parent.style.marginLeft = '0'; } } } if (this.loadFonts !== 'false') { const fontDescriptor = { family: 'Open Sans', style: 'normal', weight: '300', unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', }; const fontWeight300 = new FontFace( 'Open Sans', "url(https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UN_r8OUuhpKKSTjw.woff2) format('woff2')", fontDescriptor, ); fontDescriptor.weight = '600'; const fontWeight600 = new FontFace( 'Open Sans', "url(https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UNirkOUuhpKKSTjw.woff2) format('woff2')", fontDescriptor, ); fontWeight300.load().then((font) => { document.fonts.add(font); }); fontWeight600.load().then((font) => { document.fonts.add(font); }); } if (!this.layout || !'row, column,'.includes(`${this.layout},`)) { this.layout = 'row'; } if (!this.renderStyle || !'read, view, focused,'.includes(`${this.renderStyle},`)) { this.renderStyle = 'focused'; } if (!this.schemaStyle || !'tree, table,'.includes(`${this.schemaStyle},`)) { this.schemaStyle = 'tree'; } if (!this.theme || !'light, dark,'.includes(`${this.theme},`)) { this.theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; } if (!this.defaultSchemaTab || !'example, schema, model,'.includes(`${this.defaultSchemaTab},`)) { this.defaultSchemaTab = 'example'; } else if (this.defaultSchemaTab === 'model') { this.defaultSchemaTab = 'schema'; } if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) { this.schemaExpandLevel = 99999; } if (!this.schemaDescriptionExpanded || !'true, false,'.includes(`${this.schemaDescriptionExpanded},`)) { this.schemaDescriptionExpanded = 'false'; } if (!this.schemaHideReadOnly || !'default, never,'.includes(`${this.schemaHideReadOnly},`)) { this.schemaHideReadOnly = 'default'; } if (!this.schemaHideWriteOnly || !'default, never,'.includes(`${this.schemaHideWriteOnly},`)) { this.schemaHideWriteOnly = 'default'; } if (!this.fillRequestFieldsWithExample || !'true, false,'.includes(`${this.fillRequestFieldsWithExample},`)) { this.fillRequestFieldsWithExample = 'true'; } if (!this.persistAuth || !'true, false,'.includes(`${this.persistAuth},`)) { this.persistAuth = 'false'; } if (!this.responseAreaHeight) { this.responseAreaHeight = '400px'; } if (!this.allowSearch || !'true, false,'.includes(`${this.allowSearch},`)) { this.allowSearch = 'true'; } if (!this.allowAdvancedSearch || !'true, false,'.includes(`${this.allowAdvancedSearch},`)) { this.allowAdvancedSearch = 'true'; } if (!this.allowTry || !'true, false,'.includes(`${this.allowTry},`)) { this.allowTry = 'true'; } if (!this.apiKeyValue) { this.apiKeyValue = '-'; } if (!this.apiKeyLocation) { this.apiKeyLocation = 'header'; } if (!this.apiKeyName) { this.apiKeyName = ''; } if (!this.oauthReceiver) { this.oauthReceiver = 'oauth-receiver.html'; } if (!this.updateRoute || !'true, false,'.includes(`${this.updateRoute},`)) { this.updateRoute = 'true'; } if (!this.routePrefix) { this.routePrefix = '#'; } if (!this.sortTags || !'true, false,'.includes(`${this.sortTags},`)) { this.sortTags = 'false'; } if (!this.sortSchemas || !'true, false,'.includes(`${this.sortSchemas},`)) { this.sortSchemas = 'false'; } if (!this.generateMissingTags || !'true, false,'.includes(`${this.generateMissingTags},`)) { this.generateMissingTags = 'false'; } if (!this.sortEndpointsBy || !'method, path, summary, none,'.includes(`${this.sortEndpointsBy},`)) { this.sortEndpointsBy = 'path'; } if (!this.onNavTagClick || !'expand-collapse, show-description,'.includes(`${this.onNavTagClick},`)) { this.onNavTagClick = 'expand-collapse'; } if (!this.navItemSpacing || !'compact, relaxed, default,'.includes(`${this.navItemSpacing},`)) { this.navItemSpacing = 'default'; } if (!this.showMethodInNavBar || !'false, as-plain-text, as-colored-text, as-colored-block,'.includes(`${this.showMethodInNavBar},`)) { this.showMethodInNavBar = 'false'; } if (!this.usePathInNavBar || !'true, false,'.includes(`${this.usePathInNavBar},`)) { this.usePathInNavBar = 'false'; } if (!this.navActiveItemMarker || !'left-bar, colored-block'.includes(`${this.navActiveItemMarker},`)) { this.navActiveItemMarker = 'left-bar'; } if (!this.fontSize || !'default, large, largest,'.includes(`${this.fontSize},`)) { this.fontSize = 'default'; } if (!this.showInfo || !'true, false,'.includes(`${this.showInfo},`)) { this.showInfo = 'true'; } if (!this.allowServerSelection || !'true, false,'.includes(`${this.allowServerSelection},`)) { this.allowServerSelection = 'true'; } if (!this.allowAuthentication || !'true, false,'.includes(`${this.allowAuthentication},`)) { this.allowAuthentication = 'true'; } if (!this.allowSchemaDescriptionExpandToggle || !'true, false,'.includes(`${this.allowSchemaDescriptionExpandToggle},`)) { this.allowSchemaDescriptionExpandToggle = 'true'; } if (!this.showSideNav || !'true false'.includes(this.showSideNav)) { this.showSideNav = 'true'; } if (!this.showComponents || !'true false'.includes(this.showComponents)) { this.showComponents = 'false'; } if (!this.infoDescriptionHeadingsInNavBar || !'true, false,'.includes(`${this.infoDescriptionHeadingsInNavBar},`)) { this.infoDescriptionHeadingsInNavBar = 'false'; } if (!this.fetchCredentials || !'omit, same-origin, include,'.includes(`${this.fetchCredentials},`)) { this.fetchCredentials = ''; } if (!this.scrollBehavior || !'smooth, auto,'.includes(`${this.scrollBehavior},`)) { this.scrollBehavior = 'auto'; } if (!this.matchType || !'includes regex'.includes(this.matchType)) { this.matchType = 'includes'; } if (!this.matchPaths) { this.matchPaths = ''; } if (!this.removeEndpointsWithBadgeLabelAs) { this.removeEndpointsWithBadgeLabelAs = ''; } if (!this.showAdvancedSearchDialog) { this.showAdvancedSearchDialog = false; } if (!this.cssFile) { this.cssFile = null; } if (!this.cssClasses) { this.cssClasses = ''; } marked.setOptions({ highlight: (code, lang) => { if (Prism.languages[lang]) { return Prism.highlight(code, Prism.languages[lang], lang); } return code; }, }); window.addEventListener('hashchange', () => { this.scrollToPath(this.getElementIDFromURL()); }, true); } // Cleanup disconnectedCallback() { if (this.intersectionObserver) { this.intersectionObserver.disconnect(); } super.disconnectedCallback(); } infoDescriptionHeadingRenderer() { const renderer = new marked.Renderer(); renderer.heading = ((text, level, raw, slugger) => `<h${level} class="observe-me" id="${slugger.slug(raw)}">${text}</h${level}>`); return renderer; } render() { // return render(mainBodyTemplate(this), this.shadowRoot, { eventContext: this }); const cssLinkEl = document.querySelector(`link[href*="${this.cssFile}"]`); // adding custom style for RapiDoc if (cssLinkEl) { this.shadowRoot.appendChild(cssLinkEl.cloneNode()); } return mainBodyTemplate.call(this); } observeExpandedContent() { // Main Container const observeOverviewEls = this.shadowRoot.querySelectorAll('.observe-me'); observeOverviewEls.forEach((targetEl) => { this.intersectionObserver.observe(targetEl); }); } attributeChangedCallback(name, oldVal, newVal) { if (name === 'spec-url') { if (oldVal !== newVal) { // put it at the end of event-loop to load all the attributes window.setTimeout(async () => { await this.loadSpec(newVal); // If goto-path is provided and no location-hash is present then try to scroll there if (this.gotoPath && !window.location.hash) { this.scrollToPath(this.gotoPath); } }, 0); } } if (name === 'match-paths' || name === 'match-type' || name === 'remove-endpoints-with-badge-label-as') { if (oldVal !== newVal) { window.setTimeout(async () => { await this.loadSpec(this.specUrl); }, 0); } } if (name === 'render-style') { if (newVal === 'read') { window.setTimeout(() => { this.observeExpandedContent(); }, 100); } else { this.intersectionObserver.disconnect(); } } if (name === 'api-key-name' || name === 'api-key-location' || name === 'api-key-value') { let updateSelectedApiKey = false; let apiKeyName = ''; let apiKeyLocation = ''; let apiKeyValue = ''; if (name === 'api-key-name') { if (this.getAttribute('api-key-location') && this.getAttribute('api-key-value')) { apiKeyName = newVal; apiKeyLocation = this.getAttribute('api-key-location'); apiKeyValue = this.getAttribute('api-key-value'); updateSelectedApiKey = true; } } else if (name === 'api-key-location') { if (this.getAttribute('api-key-name') && this.getAttribute('api-key-value')) { apiKeyLocation = newVal; apiKeyName = this.getAttribute('api-key-name'); apiKeyValue = this.getAttribute('api-key-value'); updateSelectedApiKey = true; } } else if (name === 'api-key-value') { if (this.getAttribute('api-key-name') && this.getAttribute('api-key-location')) { apiKeyValue = newVal; apiKeyLocation = this.getAttribute('api-key-location'); apiKeyName = this.getAttribute('api-key-name'); updateSelectedApiKey = true; } } if (updateSelectedApiKey) { if (this.resolvedSpec) { const rapiDocApiKey = this.resolvedSpec.securitySchemes.find((v) => v.securitySchemeId === rapidocApiKey); if (!rapiDocApiKey) { this.resolvedSpec.securitySchemes.push({ securitySchemeId: rapidocApiKey, description: 'api-key provided in rapidoc element attributes', type: 'apiKey', name: apiKeyName, in: apiKeyLocation, value: apiKeyValue, finalKeyValue: apiKeyValue, }); } else { rapiDocApiKey.name = apiKeyName; rapiDocApiKey.in = apiKeyLocation; rapiDocApiKey.value = apiKeyValue; rapiDocApiKey.finalKeyValue = apiKeyValue; } this.requestUpdate(); } } } super.attributeChangedCallback(name, oldVal, newVal); } onSpecUrlChange() { this.setAttribute('spec-url', this.shadowRoot.getElementById('spec-url').value); } onSpecFileChange(e) { this.setAttribute('spec-file', this.shadowRoot.getElementById('spec-file').value); const specFile = e.target.files[0]; const reader = new FileReader(); reader.onload = () => { try { const specObj = JSON.parse(reader.result); this.loadSpec(specObj); this.shadowRoot.getElementById('spec-url').value = ''; } catch { console.error('RapiDoc: Unable to read or parse json'); // eslint-disable-line no-console } }; // Read the Text file reader.readAsText(specFile); } onFileLoadClick() { this.shadowRoot.getElementById('spec-file').click(); } onSearchChange(e) { // this.matchPaths = e.target.value; this.searchVal = e.target.value; this.resolvedSpec.tags.forEach((tag) => tag.paths.filter((path) => { if (this.searchVal) { if (getMatchedPaths(this.searchVal, path, tag.name)) { tag.expanded = true; } } })); this.resolvedSpec.components.forEach((component) => component.subComponents.filter((v) => { v.expanded = false; if (getMatchedComponents(this.searchVal, v)) { v.expanded = true; } })); this.requestUpdate(); } onClearSearch() { const searchEl = this.shadowRoot.getElementById('nav-bar-search'); searchEl.value = ''; this.searchVal = ''; this.resolvedSpec.components.forEach((component) => component.subComponents.filter((v) => { v.expanded = true; })); } onShowSearchModalClicked() { this.showAdvancedSearchDialog = true; } // Event Handler on Dialog-Box is opened async onOpenSearchDialog(e) { // Set focus to text input const inputEl = e.detail.querySelector('input'); await sleep(0); if (inputEl) { inputEl.focus(); } } // Public Method async loadSpec(specUrl) { if (!specUrl) { return; } this.searchVal = ''; try { this.resolvedSpec = { specLoadError: false, isSpecLoading: true, tags: [], }; this.loading = true; this.loadFailed = false; const spec = await ProcessSpec.call( this, specUrl, this.generateMissingTags === 'true', this.sortTags === 'true', this.sortSchemas === 'true', this.getAttribute('sort-endpoints-by'), this.getAttribute('api-key-name'), this.getAttribute('api-key-location'), this.getAttribute('api-key-value'), this.getAttribute('server-url'), this.matchPaths, this.matchType, this.removeEndpointsWithBadgeLabelAs, ); this.loading = false; this.afterSpecParsedAndValidated(spec); } catch (err) { this.loading = false; this.loadFailed = true; this.resolvedSpec = null; console.error(`RapiDoc: Unable to resolve the API spec.. ${err.message}`); // eslint-disable-line no-console } } async afterSpecParsedAndValidated(spec) { this.resolvedSpec = spec; this.selectedServer = undefined; if (this.defaultApiServerUrl) { if (this.defaultApiServerUrl === this.serverUrl) { this.selectedServer = { url: this.serverUrl, computedUrl: this.serverUrl, }; } else if (this.resolvedSpec.servers) { this.selectedServer = this.resolvedSpec.servers.find((v) => (v.url === this.defaultApiServerUrl)); } } if (!this.selectedServer) { if (this.resolvedSpec.servers) { this.selectedServer = this.resolvedSpec.servers[0]; // eslint-disable-line prefer-destructuring } } this.requestUpdate(); // eslint-disable-next-line no-await-in-loop while (!await this.updateComplete); const specLoadedEvent = new CustomEvent('spec-loaded', { detail: spec }); this.dispatchEvent(specLoadedEvent); // Initiate IntersectionObserver and put it at the end of event loop, to allow loading all the child elements (must for larger specs) this.intersectionObserver.disconnect(); if (this.renderStyle === 'read') { await sleep(100); this.observeExpandedContent(); // This will auto-highlight the selected nav-item in read-mode } this.isIntersectionObserverActive = true; // On first time Spec load, try to navigate to location hash if provided const elementId = this.getElementIDFromURL(); if (elementId) { if (this.renderStyle === 'view') { this.expandAndGotoOperation(elementId, true, true); } else { this.scrollToPath(elementId); } } else if (this.renderStyle === 'focused') { // If goto-path is provided and no location-hash is present then try to scroll to default element if (!this.gotoPath) { const defaultElementId = this.showInfo ? 'overview' : this.resolvedSpec.tags[0]?.paths[0]; this.scrollToPath(defaultElementId); } } } /** * Return the URL from where is served the RapiDoc component, removing any hash and route prefix */ getComponentBaseURL() { const { href } = window.location; // Remove end of string # or / const cleanRouterPrefix = this.routePrefix.replace(/(#|\/)$/, ''); if (!cleanRouterPrefix) { return href.split('#')[0]; } const indexOfRoutePrefix = href.lastIndexOf(cleanRouterPrefix); if (indexOfRoutePrefix === -1) { return href; } return href.slice(0, indexOfRoutePrefix); } /** * From the URL return the ID of the element whether it is in the hash or if used a router prefix without a hash */ getElementIDFromURL() { const baseURL = this.getComponentBaseURL(); const elementId = window.location.href.replace(baseURL + this.routePrefix, ''); return elementId; } replaceHistoryState(hashId) { const baseURL = this.getComponentBaseURL(); window.history.replaceState(null, null, `${baseURL}${this.routePrefix || '#'}${hashId}`); } expandAndGotoOperation(elementId, scrollToElement = true) { if (!this.resolvedSpec) { return; } // Expand full operation and tag let isExpandingNeeded = true; const tmpElementId = elementId.indexOf('#') === -1 ? elementId : elementId.substring(1); if (tmpElementId.startsWith('overview') || tmpElementId === 'servers' || tmpElementId === 'auth') { isExpandingNeeded = false; } else { for (let i = 0; i < this.resolvedSpec.tags?.length; i++) { const tag = this.resolvedSpec.tags[i]; const path = tag.paths?.find((p) => p.elementId === elementId); if (path) { if (path.expanded && tag.expanded) { isExpandingNeeded = false; } else { path.expanded = true; tag.expanded = true; } } } } if (scrollToElement) { // requestUpdate() and delay required, else we cant find element if (isExpandingNeeded) { this.requestUpdate(); } window.setTimeout(() => { const gotoEl = this.shadowRoot.getElementById(tmpElementId); if (gotoEl) { gotoEl.scrollIntoView({ behavior: this.scrollBehavior, block: 'start' }); if (this.updateRoute === 'true') { this.replaceHistoryState(tmpElementId); } } }, isExpandingNeeded ? 150 : 0); } } isValidTopId(id) { return (id.startsWith('overview') || id === 'servers' || id === 'auth'); } isValidPathId(id) { if (id === 'overview' && this.showInfo) { return true; } if (id === 'servers' && this.allowServerSelection) { return true; } if (id === 'auth' && this.allowAuthentication) { return true; } if (id.startsWith('tag--')) { return this.resolvedSpec?.tags?.find((tag) => tag.elementId === id); } return this.resolvedSpec?.tags?.find((tag) => tag.paths.find((path) => path.elementId === id)); } onIntersect(entries) { if (this.isIntersectionObserverActive === false) { return; } entries.forEach((entry) => { if (entry.isIntersecting && entry.intersectionRatio > 0) { const oldNavEl = this.shadowRoot.querySelector('.nav-bar-tag.active, .nav-bar-path.active, .nav-bar-info.active, .nav-bar-h1.active, .nav-bar-h2.active, .operations.active'); const newNavEl = this.shadowRoot.getElementById(`link-${entry.target.id}`); // Add active class in the new element if (newNavEl) { if (this.updateRoute === 'true') { this.replaceHistoryState(entry.target.id); } newNavEl.scrollIntoView({ behavior: this.scrollBehavior, block: 'center' }); newNavEl.classList.add('active'); newNavEl.part.add('section-navbar-active-item'); } // Remove active class from previous element // if it is different from the new one (edge case on loading in read render style) if (oldNavEl && oldNavEl !== newNavEl) { oldNavEl.classList.remove('active'); oldNavEl.part.remove('section-navbar-active-item'); } } }); } // Called by anchor tags created using markdown handleHref(e) { if (e.target.tagName.toLowerCase() === 'a') { if (e.target.getAttribute('href').startsWith('#')) { const gotoEl = this.shadowRoot.getElementById(e.target.getAttribute('href').replace('#', '')); if (gotoEl) { gotoEl.scrollIntoView({ behavior: this.scrollBehavior, block: 'start' }); } } } } /** * Called by * - onClick of Navigation Bar * - onClick of Advanced Search items * * Functionality: * 1. First deactivate IntersectionObserver * 2. Scroll to the element * 3. Activate IntersectionObserver (after little delay) * */ async scrollToEventTarget(event, scrollNavItemToView = true) { if (!(event.type === 'click' || (event.type === 'keyup' && event.keyCode === 13))) { return; } const navEl = event.target; if (!navEl.dataset.contentId) { return; } this.isIntersectionObserverActive = false; if (this.renderStyle === 'focused') { const requestEl = this.shadowRoot.querySelector('api-request'); if (requestEl) { requestEl.beforeNavigationFocusedMode(); } } this.scrollToPath(navEl.dataset.contentId, true, scrollNavItemToView); setTimeout(() => { this.isIntersectionObserverActive = true; }, 300); } // Public Method (scrolls to a given path and highlights the left-nav selection) async scrollToPath(elementId, expandPath = true, scrollNavItemToView = true) { if (this.renderStyle === 'focused') { // for focused mode update this.focusedElementId to update the rendering, else it wont find the needed html elements // focusedElementId will get validated in the template this.focusedElementId = elementId; await sleep(0); } if (this.renderStyle === 'view') { this.expandAndGotoOperation(elementId, expandPath, true); } else { let isValidElementId = false; const contentEl = this.shadowRoot.getElementById(elementId); if (contentEl) { isValidElementId = true; contentEl.scrollIntoView({ behavior: this.scrollBehavior, block: 'start' }); } else { isValidElementId = false; } if (isValidElementId) { // for focused style it is important to reset request-body-selection and response selection which maintains the state for in case of multiple req-body or multiple response mime-type if (this.renderStyle === 'focused') { const requestEl = this.shadowRoot.querySelector('api-request'); if (requestEl) { requestEl.afterNavigationFocusedMode(); } const responseEl = this.shadowRoot.querySelector('api-response'); if (responseEl) { responseEl.resetSelection(); } } // Update Location Hash if (this.updateRoute === 'true') { this.replaceHistoryState(elementId); } // Update NavBar View and Styles const newNavEl = this.shadowRoot.getElementById(`link-${elementId}`); if (newNavEl) { if (scrollNavItemToView) { newNavEl.scrollIntoView({ behavior: this.scrollBehavior, block: 'center' }); } await sleep(0); const oldNavEl = this.shadowRoot.querySelector('.nav-bar-tag.active, .nav-bar-path.active, .nav-bar-info.active, .nav-bar-h1.active, .nav-bar-h2.active, .operations.active'); if (oldNavEl) { oldNavEl.classList.remove('active'); oldNavEl.part.remove('active'); oldNavEl.part.remove('section-navbar-active-item'); } newNavEl.classList.add('active'); // must add the class after scrolling newNavEl.part.add('section-navbar-active-item'); // this.requestUpdate(); } } } } // Public Method - to update security-scheme of type http setHttpUserNameAndPassword(securitySchemeId, username, password) { return applyApiKey.call(this, securitySchemeId, username, password); } // Public Method - to update security-scheme of type apiKey or OAuth setApiKey(securitySchemeId, apiKeyValue) { return applyApiKey.call(this, securitySchemeId, '', '', apiKeyValue); } // Public Method removeAllSecurityKeys() { return onClearAllApiKeys.call(this); } // Public Method setApiServer(apiServerUrl) { // return apiServerUrl; return setApiServer.call(this, apiServerUrl); } // Event handler for Advanced Search text-inputs and checkboxes onAdvancedSearch(ev, delay) { const eventTargetEl = ev.target; clearTimeout(this.timeoutId); this.timeoutId = setTimeout(() => { let searchInputEl; if (eventTargetEl.type === 'text') { searchInputEl = eventTargetEl; } else { searchInputEl = eventTargetEl.closest('.advanced-search-options').querySelector('input[type=text]'); } const searchOptions = [...eventTargetEl.closest('.advanced-search-options').querySelectorAll('input:checked')].map((v) => v.id); this.advancedSearchMatches = advancedSearch(searchInputEl.value, this.resolvedSpec.tags, searchOptions); }, delay); } } customElements.define('rapi-doc', RapiDoc);