|
| 1 | +import { css, LitElement } from 'lit-element'; |
| 2 | +import marked from 'marked'; |
| 3 | +import Prism from 'prismjs'; |
| 4 | +import 'prismjs/components/prism-css'; |
| 5 | +import 'prismjs/components/prism-yaml'; |
| 6 | +import 'prismjs/components/prism-go'; |
| 7 | +import 'prismjs/components/prism-java'; |
| 8 | +import 'prismjs/components/prism-json'; |
| 9 | +import 'prismjs/components/prism-bash'; |
| 10 | +import 'prismjs/components/prism-python'; |
| 11 | +import 'prismjs/components/prism-http'; |
| 12 | +import 'prismjs/components/prism-csharp'; |
| 13 | + |
| 14 | +// Styles |
| 15 | +import FontStyles from '~/styles/font-styles'; |
| 16 | +import InputStyles from '~/styles/input-styles'; |
| 17 | +import FlexStyles from '~/styles/flex-styles'; |
| 18 | +import TableStyles from '~/styles/table-styles'; |
| 19 | +import PrismStyles from '~/styles/prism-styles'; |
| 20 | +import TabStyles from '~/styles/tab-styles'; |
| 21 | +import NavStyles from '~/styles/nav-styles'; |
| 22 | +import InfoStyles from '~/styles/info-styles'; |
| 23 | + |
| 24 | +import EndpointStyles from '~/styles/endpoint-styles'; |
| 25 | +import ProcessSpec from '~/utils/spec-parser'; |
| 26 | +import jsonSchemaViewerTemplate from '~/templates/json-schema-viewer-template'; |
| 27 | + |
| 28 | +export default class JsonSchemaViewer extends LitElement { |
| 29 | + constructor() { |
| 30 | + super(); |
| 31 | + this.isMini = false; |
| 32 | + this.updateRoute = 'false'; |
| 33 | + this.renderStyle = 'focused'; |
| 34 | + this.showHeader = 'true'; |
| 35 | + this.allowAdvancedSearch = 'false'; |
| 36 | + this.selectedExampleForEachSchema = {}; |
| 37 | + } |
| 38 | + |
| 39 | + static get properties() { |
| 40 | + return { |
| 41 | + // Spec |
| 42 | + specUrl: { type: String, attribute: 'spec-url' }, |
| 43 | + |
| 44 | + // Schema Styles |
| 45 | + schemaStyle: { type: String, attribute: 'schema-style' }, |
| 46 | + schemaExpandLevel: { type: Number, attribute: 'schema-expand-level' }, |
| 47 | + schemaDescriptionExpanded: { type: String, attribute: 'schema-description-expanded' }, |
| 48 | + allowSchemaDescriptionExpandToggle: { type: String, attribute: 'allow-schema-description-expand-toggle' }, |
| 49 | + |
| 50 | + // Hide/show Sections |
| 51 | + showHeader: { type: String, attribute: 'show-header' }, |
| 52 | + showSideNav: { type: String, attribute: 'show-side-nav' }, |
| 53 | + showInfo: { type: String, attribute: 'show-info' }, |
| 54 | + |
| 55 | + // Allow or restrict features |
| 56 | + allowSpecUrlLoad: { type: String, attribute: 'allow-spec-url-load' }, |
| 57 | + allowSpecFileLoad: { type: String, attribute: 'allow-spec-file-load' }, |
| 58 | + allowSearch: { type: String, attribute: 'allow-search' }, |
| 59 | + |
| 60 | + // Main Colors and Font |
| 61 | + theme: { type: String }, |
| 62 | + bgColor: { type: String, attribute: 'bg-color' }, |
| 63 | + textColor: { type: String, attribute: 'text-color' }, |
| 64 | + primaryColor: { type: String, attribute: 'primary-color' }, |
| 65 | + fontSize: { type: String, attribute: 'font-size' }, |
| 66 | + regularFont: { type: String, attribute: 'regular-font' }, |
| 67 | + monoFont: { type: String, attribute: 'mono-font' }, |
| 68 | + loadFonts: { type: String, attribute: 'load-fonts' }, |
| 69 | + |
| 70 | + // Internal Properties |
| 71 | + loading: { type: Boolean }, // indicates spec is being loaded |
| 72 | + }; |
| 73 | + } |
| 74 | + |
| 75 | + static get styles() { |
| 76 | + return [ |
| 77 | + FontStyles, |
| 78 | + InputStyles, |
| 79 | + FlexStyles, |
| 80 | + TableStyles, |
| 81 | + EndpointStyles, |
| 82 | + PrismStyles, |
| 83 | + TabStyles, |
| 84 | + NavStyles, |
| 85 | + InfoStyles, |
| 86 | + css` |
| 87 | + :host { |
| 88 | + display:flex; |
| 89 | + flex-direction: column; |
| 90 | + min-width:360px; |
| 91 | + width:100%; |
| 92 | + height:100%; |
| 93 | + margin:0; |
| 94 | + padding:0; |
| 95 | + overflow: hidden; |
| 96 | + letter-spacing:normal; |
| 97 | + color:var(--fg); |
| 98 | + background-color:var(--bg); |
| 99 | + font-family:var(--font-regular); |
| 100 | + } |
| 101 | + .body { |
| 102 | + display:flex; |
| 103 | + height:100%; |
| 104 | + width:100%; |
| 105 | + overflow:hidden; |
| 106 | + } |
| 107 | + .nav-bar { |
| 108 | + width: 230px; |
| 109 | + display:flex; |
| 110 | + } |
| 111 | +
|
| 112 | + .main-content { |
| 113 | + margin:0; |
| 114 | + padding: 16px; |
| 115 | + display:block; |
| 116 | + flex:1; |
| 117 | + height:100%; |
| 118 | + overflow-y: auto; |
| 119 | + overflow-x: hidden; |
| 120 | + scrollbar-width: thin; |
| 121 | + scrollbar-color: var(--border-color) transparent; |
| 122 | + } |
| 123 | + .main-content-inner--view-mode { |
| 124 | + padding: 0 8px; |
| 125 | + } |
| 126 | + .main-content::-webkit-scrollbar { |
| 127 | + width: 8px; |
| 128 | + height: 8px; |
| 129 | + } |
| 130 | + .main-content::-webkit-scrollbar-track { |
| 131 | + background:transparent; |
| 132 | + } |
| 133 | + .main-content::-webkit-scrollbar-thumb { |
| 134 | + background-color: var(--border-color); |
| 135 | + } |
| 136 | + .header { |
| 137 | + background-color:var(--header-bg); |
| 138 | + color:var(--header-fg); |
| 139 | + width:100%; |
| 140 | + } |
| 141 | + .header-title { |
| 142 | + font-size:calc(var(--font-size-regular) + 8px); |
| 143 | + padding:0 8px; |
| 144 | + } |
| 145 | + input.header-input{ |
| 146 | + background:var(--header-color-darker); |
| 147 | + color:var(--header-fg); |
| 148 | + border:1px solid var(--header-color-border); |
| 149 | + flex:1; |
| 150 | + padding-right:24px; |
| 151 | + border-radius:3px; |
| 152 | + } |
| 153 | + input.header-input::placeholder { |
| 154 | + opacity:0.4; |
| 155 | + } |
| 156 | + .loader { |
| 157 | + margin: 16px auto 16px auto; |
| 158 | + border: 4px solid var(--bg3); |
| 159 | + border-radius: 50%; |
| 160 | + border-top: 4px solid var(--primary-color); |
| 161 | + width: 36px; |
| 162 | + height: 36px; |
| 163 | + animation: spin 2s linear infinite; |
| 164 | + } |
| 165 | + @media only screen and (min-width: 768px) { |
| 166 | + .only-large-screen{ |
| 167 | + display:block; |
| 168 | + } |
| 169 | + .only-large-screen-flex{ |
| 170 | + display:flex; |
| 171 | + } |
| 172 | + }`, |
| 173 | + ]; |
| 174 | + } |
| 175 | + |
| 176 | + // Startup |
| 177 | + connectedCallback() { |
| 178 | + super.connectedCallback(); |
| 179 | + const parent = this.parentElement; |
| 180 | + if (parent) { |
| 181 | + if (parent.offsetWidth === 0 && parent.style.width === '') { |
| 182 | + parent.style.width = '100vw'; |
| 183 | + } |
| 184 | + if (parent.offsetHeight === 0 && parent.style.height === '') { |
| 185 | + parent.style.height = '100vh'; |
| 186 | + } |
| 187 | + if (parent.tagName === 'BODY') { |
| 188 | + if (!parent.style.marginTop) { parent.style.marginTop = '0'; } |
| 189 | + if (!parent.style.marginRight) { parent.style.marginRight = '0'; } |
| 190 | + if (!parent.style.marginBottom) { parent.style.marginBottom = '0'; } |
| 191 | + if (!parent.style.marginLeft) { parent.style.marginLeft = '0'; } |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + if (this.loadFonts !== 'false') { |
| 196 | + const fontDescriptor = { |
| 197 | + family: 'Open Sans', |
| 198 | + style: 'normal', |
| 199 | + weight: '300', |
| 200 | + 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', |
| 201 | + }; |
| 202 | + const fontWeight300 = new FontFace( |
| 203 | + 'Open Sans', |
| 204 | + "url(https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UN_r8OUuhpKKSTjw.woff2) format('woff2')", |
| 205 | + fontDescriptor, |
| 206 | + ); |
| 207 | + fontDescriptor.weight = '600'; |
| 208 | + const fontWeight600 = new FontFace( |
| 209 | + 'Open Sans', |
| 210 | + "url(https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UNirkOUuhpKKSTjw.woff2) format('woff2')", |
| 211 | + fontDescriptor, |
| 212 | + ); |
| 213 | + fontWeight300.load().then((font) => { document.fonts.add(font); }); |
| 214 | + fontWeight600.load().then((font) => { document.fonts.add(font); }); |
| 215 | + } |
| 216 | + |
| 217 | + this.renderStyle = 'focused'; |
| 218 | + this.pathsExpanded = this.pathsExpanded === 'true'; |
| 219 | + |
| 220 | + if (!this.showInfo || !'true, false,'.includes(`${this.showInfo},`)) { this.showInfo = 'true'; } |
| 221 | + if (!this.showSideNav || !'true false'.includes(this.showSideNav)) { this.showSideNav = 'true'; } |
| 222 | + if (!this.showHeader || !'true, false,'.includes(`${this.showHeader},`)) { this.showHeader = 'true'; } |
| 223 | + |
| 224 | + if (!this.schemaStyle || !'tree, table,'.includes(`${this.schemaStyle},`)) { this.schemaStyle = 'tree'; } |
| 225 | + if (!this.theme || !'light, dark,'.includes(`${this.theme},`)) { |
| 226 | + this.theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; |
| 227 | + } |
| 228 | + if (!this.allowSearch || !'true, false,'.includes(`${this.allowSearch},`)) { this.allowSearch = 'true'; } |
| 229 | + if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) { this.schemaExpandLevel = 99999; } |
| 230 | + if (!this.schemaDescriptionExpanded || !'true, false,'.includes(`${this.schemaDescriptionExpanded},`)) { this.schemaDescriptionExpanded = 'false'; } |
| 231 | + if (!this.responseAreaHeight) { this.responseAreaHeight = '300px'; } |
| 232 | + if (!this.fontSize || !'default, large, largest,'.includes(`${this.fontSize},`)) { this.fontSize = 'default'; } |
| 233 | + if (!this.matchType || !'includes regex'.includes(this.matchType)) { this.matchType = 'includes'; } |
| 234 | + if (!this.allowSchemaDescriptionExpandToggle || !'true, false,'.includes(`${this.allowSchemaDescriptionExpandToggle},`)) { this.allowSchemaDescriptionExpandToggle = 'true'; } |
| 235 | + |
| 236 | + marked.setOptions({ |
| 237 | + highlight: (code, lang) => { |
| 238 | + if (Prism.languages[lang]) { |
| 239 | + return Prism.highlight(code, Prism.languages[lang], lang); |
| 240 | + } |
| 241 | + return code; |
| 242 | + }, |
| 243 | + }); |
| 244 | + } |
| 245 | + |
| 246 | + render() { |
| 247 | + return jsonSchemaViewerTemplate.call(this, true, false, false, this.pathsExpanded); |
| 248 | + } |
| 249 | + |
| 250 | + attributeChangedCallback(name, oldVal, newVal) { |
| 251 | + if (name === 'spec-url') { |
| 252 | + if (oldVal !== newVal) { |
| 253 | + // put it at the end of event-loop to load all the attributes |
| 254 | + window.setTimeout(async () => { |
| 255 | + await this.loadSpec(newVal); |
| 256 | + }, 0); |
| 257 | + } |
| 258 | + } |
| 259 | + super.attributeChangedCallback(name, oldVal, newVal); |
| 260 | + } |
| 261 | + |
| 262 | + onSepcUrlChange() { |
| 263 | + this.setAttribute('spec-url', this.shadowRoot.getElementById('spec-url').value); |
| 264 | + } |
| 265 | + |
| 266 | + onSearchChange(e) { |
| 267 | + // Todo: Filter Search |
| 268 | + this.matchPaths = e.target.value; |
| 269 | + } |
| 270 | + |
| 271 | + // Public Method |
| 272 | + async loadSpec(specUrl) { |
| 273 | + if (!specUrl) { |
| 274 | + return; |
| 275 | + } |
| 276 | + try { |
| 277 | + this.resolvedSpec = { |
| 278 | + specLoadError: false, |
| 279 | + isSpecLoading: true, |
| 280 | + tags: [], |
| 281 | + }; |
| 282 | + this.loading = true; |
| 283 | + this.loadFailed = false; |
| 284 | + this.requestUpdate(); |
| 285 | + const spec = await ProcessSpec.call( |
| 286 | + this, |
| 287 | + specUrl, |
| 288 | + this.generateMissingTags === 'true', |
| 289 | + this.sortTags === 'true', |
| 290 | + this.getAttribute('sort-endpoints-by'), |
| 291 | + ); |
| 292 | + this.loading = false; |
| 293 | + this.afterSpecParsedAndValidated(spec); |
| 294 | + } catch (err) { |
| 295 | + this.loading = false; |
| 296 | + this.loadFailed = true; |
| 297 | + this.resolvedSpec = null; |
| 298 | + console.error(`RapiDoc: Unable to resolve the API spec.. ${err.message}`); // eslint-disable-line no-console |
| 299 | + } |
| 300 | + } |
| 301 | + |
| 302 | + async afterSpecParsedAndValidated(spec) { |
| 303 | + this.resolvedSpec = spec; |
| 304 | + const specLoadedEvent = new CustomEvent('spec-loaded', { detail: spec }); |
| 305 | + this.dispatchEvent(specLoadedEvent); |
| 306 | + } |
| 307 | + |
| 308 | + // Called by anchor tags created using markdown |
| 309 | + handleHref(e) { |
| 310 | + if (e.target.tagName.toLowerCase() === 'a') { |
| 311 | + if (e.target.getAttribute('href').startsWith('#')) { |
| 312 | + const gotoEl = this.shadowRoot.getElementById(e.target.getAttribute('href').replace('#', '')); |
| 313 | + if (gotoEl) { |
| 314 | + gotoEl.scrollIntoView({ behavior: 'auto', block: 'start' }); |
| 315 | + } |
| 316 | + } |
| 317 | + } |
| 318 | + } |
| 319 | + |
| 320 | + // Example Dropdown @change Handler |
| 321 | + onSelectExample(e) { |
| 322 | + const exampleContainerEl = e.target.closest('.json-schema-example-panel'); |
| 323 | + const exampleEls = [...exampleContainerEl.querySelectorAll('.example')]; |
| 324 | + exampleEls.forEach((v) => { |
| 325 | + v.style.display = v.dataset.example === e.target.value ? 'flex' : 'none'; |
| 326 | + }); |
| 327 | + } |
| 328 | + |
| 329 | + async scrollToEventTarget(event) { |
| 330 | + const navEl = event.currentTarget; |
| 331 | + if (!navEl.dataset.contentId) { |
| 332 | + return; |
| 333 | + } |
| 334 | + const contentEl = this.shadowRoot.getElementById(navEl.dataset.contentId); |
| 335 | + if (contentEl) { |
| 336 | + contentEl.scrollIntoView({ behavior: 'auto', block: 'start' }); |
| 337 | + } |
| 338 | + } |
| 339 | +} |
| 340 | +customElements.define('json-schema-viewer', JsonSchemaViewer); |
0 commit comments