rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
368 lines (354 loc) • 17 kB
JavaScript
import { LitElement, html, css } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js'; // eslint-disable-line import/extensions
import { marked } from 'marked';
import FontStyles from '~/styles/font-styles';
import SchemaStyles from '~/styles/schema-styles';
import CustomStyles from '~/styles/custom-styles';
export default class SchemaTable extends LitElement {
static get properties() {
return {
schemaExpandLevel: { type: Number, attribute: 'schema-expand-level' },
schemaDescriptionExpanded: { type: String, attribute: 'schema-description-expanded' },
allowSchemaDescriptionExpandToggle: { type: String, attribute: 'allow-schema-description-expand-toggle' },
schemaHideReadOnly: { type: String, attribute: 'schema-hide-read-only' },
schemaHideWriteOnly: { type: String, attribute: 'schema-hide-write-only' },
data: { type: Object },
};
}
connectedCallback() {
super.connectedCallback();
if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) { this.schemaExpandLevel = 99999; }
if (!this.schemaDescriptionExpanded || !'true false'.includes(this.schemaDescriptionExpanded)) { this.schemaDescriptionExpanded = 'false'; }
if (!this.schemaHideReadOnly || !'true false'.includes(this.schemaHideReadOnly)) { this.schemaHideReadOnly = 'true'; }
if (!this.schemaHideWriteOnly || !'true false'.includes(this.schemaHideWriteOnly)) { this.schemaHideWriteOnly = 'true'; }
}
static get styles() {
return [
FontStyles,
SchemaStyles,
css`
.table {
font-size: var(--font-size-small);
text-align: left;
line-height: calc(var(--font-size-small) + 6px);
}
.table .tr {
width: calc(100% - 5px);
padding: 0 0 0 5px;
border-bottom: 1px dotted var(--light-border-color);
}
.table .td {
padding: 4px 0;
}
.table .key {
width: 240px;
}
.key .key-label {
font-size: var(--font-size-mono);
}
.key.deprecated .key-label {
color: var(--red);
}
.table .key-type {
white-space: normal;
width: 150px;
}
.collapsed-all-descr .tr:not(.expanded-descr) {
max-height: calc(var(--font-size-small) + var(--font-size-small));
}
.obj-toggle {
padding: 0 2px;
border-radius:2px;
border: 1px solid transparent;
display: inline-block;
margin-left: -16px;
color:var(--primary-color);
cursor:pointer;
font-size: calc(var(--font-size-small) + 4px);
font-family: var(--font-mono);
background-clip: border-box;
}
.obj-toggle:hover {
border-color: var(--primary-color);
}
.tr.expanded + .object-body {
display:block;
}
.tr.collapsed + .object-body {
display:none;
}`,
CustomStyles,
];
}
/* eslint-disable indent */
render() {
return html`
<div class="table ${this.schemaDescriptionExpanded === 'true' ? 'expanded-all-descr' : 'collapsed-all-descr'}" @click="${(e) => this.handleAllEvents(e)}">
<div class='toolbar'>
<div class="toolbar-item schema-root-type ${this.data?.['::type'] || ''} "> ${this.data?.['::type'] || ''} </div>
${this.allowSchemaDescriptionExpandToggle === 'true'
? html`
<div style="flex:1"></div>
<div part="schema-multiline-toggle" class='toolbar-item schema-multiline-toggle' >
${this.schemaDescriptionExpanded === 'true' ? 'Single line description' : 'Multiline description'}
</div>
`
: ''
}
</div>
<span part="schema-description" class='m-markdown'> ${unsafeHTML(marked(this.data?.['::description'] || ''))} </span>
<div style = 'border:1px solid var(--light-border-color)'>
<div style='display:flex; background-color: var(--bg2); padding:8px 4px; border-bottom:1px solid var(--light-border-color);'>
<div class='key' style='font-family:var(--font-regular); font-weight:bold; color:var(--fg);'> Field </div>
<div class='key-type' style='font-family:var(--font-regular); font-weight:bold; color:var(--fg);'> Type </div>
<div class='key-descr' style='font-family:var(--font-regular); font-weight:bold; color:var(--fg);'> Description </div>
</div>
${this.data
? html`
${this.generateTree(
this.data['::type'] === 'array' ? this.data['::props'] : this.data,
this.data['::type'],
this.data['::array-type'],
)}`
: ''
}
</div>
</div>
`;
}
generateTree(data, dataType = 'object', arrayType = '', key = '', description = '', schemaLevel = 0, indentLevel = 0, readOrWrite = '', isDeprecated = false) {
if (this.schemaHideReadOnly === 'true') {
if (dataType === 'array') {
if (readOrWrite === 'readonly') {
return;
}
}
if (data && data['::readwrite'] === 'readonly') {
return;
}
}
if (this.schemaHideWriteOnly === 'true') {
if (dataType === 'array') {
if (readOrWrite === 'writeonly') {
return;
}
}
if (data && data['::readwrite'] === 'writeonly') {
return;
}
}
if (!data) {
return html`<div class="null" style="display:inline;">
<span style='margin-left:${(schemaLevel + 1) * 16}px'> </span>
<span class="key-label xxx-of-key"> ${key.replace('::OPTION~', '')}</span>
${
dataType === 'array'
? html`<span class='mono-font'> [ ] </span>`
: dataType === 'object'
? html`<span class='mono-font'> { } </span>`
: html`<span class='mono-font'> schema undefined </span>`
}
</div>`;
}
const newSchemaLevel = data['::type']?.startsWith('xxx-of') ? schemaLevel : (schemaLevel + 1);
const newIndentLevel = dataType === 'xxx-of-option' || data['::type'] === 'xxx-of-option' || key.startsWith('::OPTION') ? indentLevel : (indentLevel + 1);
const leftPadding = 16 * newIndentLevel; // 2 space indentation at each level
if (Object.keys(data).length === 0) {
return html`<span class="td key object" style='padding-left:${leftPadding}px'>${key}</span>`;
}
let keyLabel = '';
let keyDescr = '';
let isOneOfLabel = false;
if (key.startsWith('::ONE~OF') || key.startsWith('::ANY~OF')) {
keyLabel = key.replace('::', '').replace('~', ' ');
isOneOfLabel = true;
} else if (key.startsWith('::OPTION')) {
const parts = key.split('~');
keyLabel = parts[1]; // eslint-disable-line prefer-destructuring
keyDescr = parts[2]; // eslint-disable-line prefer-destructuring
} else {
keyLabel = key;
}
let detailObjType = '';
if (data['::type'] === 'object') {
if (dataType === 'array') {
detailObjType = 'array of object'; // Array of Object
} else {
detailObjType = data['::dataTypeLabel'] || data['::type'];
}
} else if (data['::type'] === 'array') {
if (dataType === 'array') {
// detailObjType = 'array of array'; // Array of array
detailObjType = `array of array ${arrayType !== 'object' ? `of ${arrayType}` : ''}`; // Array of array
} else {
detailObjType = data['::dataTypeLabel'] || data['::type'];
}
}
if (typeof data === 'object') {
return html`
${newSchemaLevel >= 0 && key
? html`
<div class='tr ${newSchemaLevel <= this.schemaExpandLevel ? 'expanded' : 'collapsed'} ${data['::type']}' data-obj='${keyLabel}' title="${isDeprecated || data['::deprecated'] ? 'Deprecated' : ''}">
<div class="td key ${(isDeprecated || data['::deprecated']) ? 'deprecated' : ''}" style='padding-left:${leftPadding}px'>
${(keyLabel || keyDescr)
? html`
<span class='obj-toggle ${newSchemaLevel < this.schemaExpandLevel ? 'expanded' : 'collapsed'}' data-obj='${keyLabel}'>
${schemaLevel < this.schemaExpandLevel ? '-' : '+'}
</span>`
: ''
}
${data['::type'] === 'xxx-of-option' || data['::type'] === 'xxx-of-array' || key.startsWith('::OPTION')
? html`<span class="xxx-of-key" style="margin-left:-6px">${keyLabel}</span><span class="${isOneOfLabel ? 'xxx-of-key' : 'xxx-of-descr'}">${keyDescr}</span>`
: keyLabel.endsWith('*')
? html`<span class="key-label" style="display:inline-block; margin-left:-6px;">${(isDeprecated || data['::deprecated'])
? html`<svg viewBox="0 0 10 10" width="10" height="10" style="stroke:var(--red); margin-right:-6px"><path d="M2 2L8 8M2 8L8 2"/></svg>`
: ''
} ${keyLabel.substring(0, keyLabel.length - 1)}</span><span style='color:var(--red);'>*</span>`
: html`<span class="key-label" style="display:inline-block; margin-left:-6px;">${(isDeprecated || data['::deprecated'])
? html`<svg viewBox="0 0 10 10" width="10" height="10" style="stroke:var(--red); margin-right:-6px"><path d="M2 2L8 8M2 8L8 2"/></svg>`
: ''
} ${keyLabel === '::props' ? '' : keyLabel}</span>`
}
${data['::type'] === 'xxx-of' && dataType === 'array' ? html`<span style="color:var(--primary-color)">ARRAY</span>` : ''}
</div>
<div class='td key-type' title="${data['::readwrite'] === 'readonly' ? 'Read-Only' : data['::readwrite'] === 'writeonly' ? 'Write-Only' : ''}">
${(data['::type'] || '').includes('xxx-of') ? '' : detailObjType}
${data['::readwrite'] === 'readonly' ? ' 🆁' : data['::readwrite'] === 'writeonly' ? ' 🆆' : ''}
</div>
<div class='td key-descr m-markdown-small' style='line-height:1.7'>${unsafeHTML(marked(description || ''))}</div>
</div>`
: html`
${data['::type'] === 'array' && dataType === 'array'
? html`
<div class='tr'>
<div class='td key'></div>
<div class='td key-type'>
${arrayType && arrayType !== 'object' ? `${dataType} of ${arrayType}` : dataType}
</div>
<div class='td key-descr'></div>
</div>`
: ''
}`
}
<div class='object-body'>
${Array.isArray(data) && data[0]
? html`${this.generateTree(data[0], 'xxx-of-option', '', '::ARRAY~OF', '', newSchemaLevel, newIndentLevel, '')}`
: html`
${Object.keys(data).map((dataKey) => html`
${['::title', '::description', '::type', '::props', '::deprecated', '::array-type', '::readwrite', '::dataTypeLabel', '::nullable'].includes(dataKey)
? data[dataKey]['::type'] === 'array' || data[dataKey]['::type'] === 'object'
? html`${this.generateTree(
data[dataKey]['::type'] === 'array' ? data[dataKey]['::props'] : data[dataKey],
data[dataKey]['::type'],
data[dataKey]['::array-type'] || '',
dataKey,
data[dataKey]['::description'],
newSchemaLevel,
newIndentLevel,
data[dataKey]['::readwrite'] ? data[dataKey]['::readwrite'] : '',
(isDeprecated || data[dataKey]['::deprecated']),
)}`
: ''
: html`${this.generateTree(
data[dataKey]['::type'] === 'array' ? data[dataKey]['::props'] : data[dataKey],
data[dataKey]['::type'],
data[dataKey]['::array-type'] || '',
dataKey,
data[dataKey]?.['::description'] || '',
newSchemaLevel,
newIndentLevel,
data[dataKey]['::readwrite'] ? data[dataKey]['::readwrite'] : '',
(isDeprecated || data[dataKey]['::deprecated']),
)}`
}
`)}
`
}
</div>
`;
}
// For Primitive Data types
// eslint-disable-next-line no-unused-vars
const [type, readOrWriteOnly, constraint, defaultValue, allowedValues, pattern, schemaDescription, schemaTitle, deprecated] = data.split('~|~');
if (readOrWriteOnly === '🆁' && this.schemaHideReadOnly === 'true') {
return;
}
if (readOrWriteOnly === '🆆' && this.schemaHideWriteOnly === 'true') {
return;
}
const dataTypeCss = type.replace(/┃.*/g, '').replace(/[^a-zA-Z0-9+]/g, '').substring(0, 4).toLowerCase();
const descrExpander = `${constraint || defaultValue || allowedValues || pattern ? '<span class="descr-expand-toggle">➔</span>' : ''}`;
let dataTypeHtml = '';
if (dataType === 'array') {
dataTypeHtml = html`
<div class='td key-type ${dataTypeCss}' title="${readOrWrite === 'readonly' ? 'Read-Only' : readOrWriteOnly === 'writeonly' ? 'Write-Only' : ''}">
[${type}] ${readOrWrite === 'readonly' ? '🆁' : readOrWrite === 'writeonly' ? '🆆' : ''}
</div>`;
} else {
dataTypeHtml = html`
<div class='td key-type ${dataTypeCss}' title="${readOrWriteOnly === '🆁' ? 'Read-Only' : readOrWriteOnly === '🆆' ? 'Write-Only' : ''}">
${type} ${readOrWriteOnly}
</div>`;
}
return html`
<div class = "tr primitive" title="${isDeprecated || deprecated ? 'Deprecated' : ''}">
<div class="td key ${isDeprecated || deprecated ? 'deprecated' : ''}" style='padding-left:${leftPadding}px'>
${isDeprecated || deprecated
? html`<svg viewBox="0 0 10 10" width="10" height="10" style="stroke:var(--red); margin-right:-6px"><path d="M2 2L8 8M2 8L8 2"/></svg>`
: ''
}
${keyLabel?.endsWith('*')
? html`
<span class="key-label">${keyLabel.substring(0, keyLabel.length - 1)}</span>
<span style='color:var(--red);'>*</span>`
: key.startsWith('::OPTION')
? html`<span class='xxx-of-key'>${keyLabel}</span><span class="xxx-of-descr">${keyDescr}</span>`
: html`${keyLabel ? html`<span class="key-label"> ${keyLabel}</span>` : html`<span class="xxx-of-descr">${schemaTitle}</span>`}`
}
</div>
${dataTypeHtml}
<div class='td key-descr' style='font-size: var(--font-size-small)'>
${html`<span class="m-markdown-small">
${unsafeHTML(marked(dataType === 'array'
? `${descrExpander} ${description}`
: schemaTitle
? `${descrExpander} <b>${schemaTitle}:</b> ${schemaDescription}`
: `${descrExpander} ${schemaDescription}`))}
</span>`
}
${constraint ? html`<div class='' style='display:inline-block; line-break:anywhere; margin-right:8px;'> <span class='bold-text'>Constraints: </span> ${constraint}</div>` : ''}
${defaultValue ? html`<div style='display:inline-block; line-break:anywhere; margin-right:8px;'> <span class='bold-text'>Default: </span>${defaultValue}</div>` : ''}
${allowedValues ? html`<div style='display:inline-block; line-break:anywhere; margin-right:8px;'> <span class='bold-text'>${type === 'const' ? 'Value' : 'Allowed'}: </span>${allowedValues}</div>` : ''}
${pattern ? html`<div style='display:inline-block; line-break:anywhere; margin-right:8px;'> <span class='bold-text'>Pattern: </span>${pattern}</div>` : ''}
</div>
</div>
`;
}
/* eslint-enable indent */
handleAllEvents(e) {
if (e.target.classList.contains('obj-toggle')) {
this.toggleObjectExpand(e);
} else if (e.target.classList.contains('schema-multiline-toggle')) {
this.schemaDescriptionExpanded = (this.schemaDescriptionExpanded === 'true' ? 'false' : 'true');
} else if (e.target.classList.contains('descr-expand-toggle')) {
const trEl = e.target.closest('.tr');
if (trEl) {
trEl.classList.toggle('expanded-descr');
trEl.style.maxHeight = trEl.scrollHeight;
}
}
}
toggleObjectExpand(e) {
const rowEl = e.target.closest('.tr');
if (rowEl.classList.contains('expanded')) {
rowEl.classList.add('collapsed');
rowEl.classList.remove('expanded');
e.target.innerText = '+';
} else {
rowEl.classList.remove('collapsed');
rowEl.classList.add('expanded');
e.target.innerText = '-';
}
}
}
customElements.define('schema-table', SchemaTable);