Skip to content

Commit 9e1ba18

Browse files
committed
[new-feature] added new json-schema-viewer component
1 parent bc47414 commit 9e1ba18

File tree

10 files changed

+557
-16
lines changed

10 files changed

+557
-16
lines changed

src/components/api-request.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ export default class ApiRequest extends LitElement {
312312
true,
313313
true,
314314
'text',
315+
false,
315316
)[0].exampleValue;
316317
}
317318
const labelColWidth = 'read focused'.includes(this.renderStyle) ? '200px' : '160px';
@@ -538,6 +539,7 @@ export default class ApiRequest extends LitElement {
538539
false,
539540
true,
540541
'text',
542+
false,
541543
);
542544
if (!this.selectedRequestBodyExample) {
543545
this.selectedRequestBodyExample = (reqBodyExamples.length > 0 ? reqBodyExamples[0].exampleId : '');
@@ -598,6 +600,7 @@ export default class ApiRequest extends LitElement {
598600
false,
599601
true,
600602
'text',
603+
false,
601604
);
602605
if (reqBody.schema) {
603606
reqBodyFormHtml = this.formDataTemplate(reqBody.schema, reqBody.mimeType, (ex[0] ? ex[0].exampleValue : ''));
@@ -689,6 +692,7 @@ export default class ApiRequest extends LitElement {
689692
false,
690693
true,
691694
'text',
695+
false,
692696
);
693697

694698
return html`

src/components/json-tree.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default class JsonTree extends LitElement {
2323
display:flex;
2424
}
2525
.json-tree {
26+
position: relative;
2627
font-family: var(--font-mono);
2728
font-size: var(--font-size-small);
2829
display:inline-block;

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import RapiDoc from '~/rapidoc';
22
import RapiDocMini from '~/rapidoc-mini';
33
import OAuthReceiver from '~/oauth-receiver';
4+
import JsonSchemaViewer from '~/json-schema-viewer';
45

56
export default { RapiDoc };
6-
export { RapiDocMini, OAuthReceiver };
7+
export { RapiDocMini, OAuthReceiver, JsonSchemaViewer };

src/json-schema-viewer.js

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
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

Comments
 (0)