rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
476 lines (446 loc) • 18.1 kB
text/typescript
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import FontStyles from "../styles/font-styles.js";
import SchemaStyles from "../styles/schema-styles.js";
import CustomStyles from "../styles/custom-styles.js";
import SchemaBaseElement from "./schema-base-element";
("schema-table")
export default class SchemaTable extends SchemaBaseElement {
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-container {
border: 1px solid var(--border);
/* Card surface, same as .param-list */
background: color-mix(in srgb, var(--card) 80%, transparent);
overflow-x: auto;
max-width: 100%;
}
& .table-header {
display: flex;
background: var(--card);
padding: 8px 4px;
border-bottom: 1px solid var(--border);
& .td {
font-family: var(--font-sans);
font-weight: bold;
color: var(--foreground);
}
}
& .tr {
width: calc(100% - 5px);
padding: 0 0 0 5px;
border-bottom: 1px dotted var(--border);
&.expanded {
& .obj-toggle::after {
content: '-';
}
& + .object-body {
display: block;
}
}
&.collapsed {
& .obj-toggle::after {
content: '+';
}
& + .object-body {
display: none;
}
}
}
& .td {
padding: 4px 0;
}
& .key {
width: var(--table-schema-key-width);
text-overflow: var(--table-schema-key-text-overflow);
white-space: var(--table-schema-key-whitespace);
overflow: hidden;
& .key-label {
font-size: var(--font-size-mono);
}
&.deprecated .key-label {
color: var(--destructive);
}
}
& .key-type {
white-space: normal;
width: 150px;
}
&.collapsed-all-descr .tr:not(.expanded-descr) {
overflow: hidden;
max-height: calc(var(--font-size-small) + 16px);
}
}
.obj-toggle {
background: transparent;
border: 1px solid transparent;
padding: 0;
border-radius: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: -16px;
margin-top: -2px;
color: var(--primary);
cursor: pointer;
font-size: 14px;
font-family: var(--font-mono);
line-height: 12px;
width: 14px;
height: 14px;
min-width: 14px;
text-align: center;
user-select: none;
box-sizing: border-box;
vertical-align: middle;
&:hover {
border-color: var(--primary);
}
&:focus-visible {
outline: 2px solid var(--primary);
border-color: var(--primary);
}
}
@supports (grid-template-columns: subgrid) {
.table {
& .table-container {
display: grid;
grid-template-columns: minmax(var(--table-schema-key-width), auto) minmax(150px, auto) 1fr;
}
& .table-header,
& .tr {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
width: 100%;
padding: 0 4px;
box-sizing: border-box;
& .key,
& .key-type {
width: auto;
}
& .key-descr {
min-width: 0;
}
}
& .object-body {
display: contents;
}
& .tr.collapsed + .object-body {
display: none;
}
& .tr.expanded + .object-body {
display: contents;
}
}
}
/* Narrow schemas: the description spans the row, under the field and type, with no column of its own (#1074) */
@container (width < 500px) {
.table {
/* no column for descriptions, and field and type share the width instead of scrolling sideways */
& .table-container {
grid-template-columns: minmax(0, 1fr) minmax(0, max-content) 0;
}
& .tr > .key-descr {
grid-column: 1 / -1;
}
& .table-header > .key-descr {
display: none;
}
&.collapsed-all-descr .tr:not(.expanded-descr) {
max-height: calc(2 * (var(--font-size-small) + 16px));
}
}
}
`,
CustomStyles,
];
}
render() {
if (!this.data) {
return "";
}
const rootType = this.data.kind === "primitive" ? this.data.type : this.data.kind || "";
const rootDescription = this.data.description || "";
return html`
<div
class="table ${this.schemaDescriptionExpanded === "true" ? "expanded-all-descr" : "collapsed-all-descr"}"
@click="${(e) => this.handleAllEvents(e)}"
@keydown="${(e) => this.handleKeyDown(e)}"
>
${this.renderToolbar(rootType)}
<span part="schema-description" class="m-markdown"> ${this.renderDescriptionHtml(rootDescription)} </span>
<div class="table-container">
<div class="tr table-header">
<div class="td key">Field</div>
<div class="td key-type">Type</div>
<div class="td key-descr">Description</div>
</div>
${this.renderAST(this.data)}
</div>
</div>
`;
}
renderAST(node, parentType = "", schemaLevel = 0, indentLevel = 0) {
if (this.isFieldHidden(node)) {
return "";
}
const leftPadding = 16 * (indentLevel + 1);
const isExpanded = schemaLevel < this.schemaExpandLevel;
const nullClass = node.nullable ? "nullable" : "";
const deprecatedIcon = this.renderDeprecatedIcon(node);
const readWriteBadge = this.getReadWriteBadge(node);
const readWriteTip = this.getReadWriteTip(node);
// 1. Array kind
if (node.kind === "array") {
const items = node.items;
if (!items) {
const typeLabel = node.dataTypeLabel || (node.nullable ? "array ┃ null" : "array");
return html`
<div class="tr array ${nullClass}" data-obj="${node.name || ""}">
<div class="td key" style="padding-left:${leftPadding}px">
<span class="key-label" title="${node.name || nothing}">${deprecatedIcon}${node.name || ""}</span>
${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}
</div>
<div class="td key-type" title="${readWriteTip || nothing}">${typeLabel}${readWriteBadge}</div>
${this.renderDescrCell(node.description)}
</div>
`;
}
if (items.kind === "primitive") {
const itemType = items.format || items.contentMediaType || items.type || "string";
const dataTypeCss = this.getDataTypeCss(itemType);
return html`
<div class="tr primitive ${nullClass}" title="${node.deprecated || items.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated || items.deprecated ? "deprecated" : ""}" style="padding-left:${leftPadding}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${node.name}">${node.name}</span>` : ""}
${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}
</div>
<div class="td key-type ${dataTypeCss}" title="${readWriteTip || nothing}">
[${itemType}]${node.nullable ? " ┃ null" : ""}${readWriteBadge}
</div>
${this.renderDescrCell(node.description || items.description, items)}
</div>
`;
}
let arrLabel = "array";
if (items.kind === "object") {
arrLabel = "array of object";
} else if (items.kind === "array") {
arrLabel = `array of array ${node.arrayType ? `of ${node.arrayType}` : ""}`.trim();
} else if (items.kind === "union") {
arrLabel = "array of union";
}
if (node.nullable) {
arrLabel += " ┃ null";
}
const hasChildren =
items.kind === "object"
? !!(
(items.properties && items.properties.length > 0) ||
(items.patternProperties && items.patternProperties.length > 0) ||
items.additionalProperties ||
(items.unions && items.unions.length > 0)
)
: true;
return html`
<div
class="tr ${isExpanded ? "expanded" : "collapsed"} array ${nullClass}"
data-obj="${node.name || ""}"
title="${node.deprecated ? `Deprecated ${node.name}` : node.name || nothing}"
>
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="padding-left:${leftPadding}px">
${
node.name && hasChildren
? html`<button
type="button"
class="obj-toggle"
data-obj="${node.name}"
aria-label="Toggle ${node.name}"
aria-expanded="${isExpanded ? "true" : "false"}"
></button>`
: ""
}
<span class="key-label" title="${node.name || nothing}" style="display:inline-block; ${hasChildren ? "margin-left:-6px;" : ""}">
${deprecatedIcon}${node.name || ""}
</span>
${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}
</div>
<div class="td key-type" title="${readWriteTip || nothing}">${arrLabel}${readWriteBadge}</div>
${this.renderDescrCell(node.description || items.description)}
</div>
${
hasChildren
? html`
<div class="object-body">
${
items.kind === "object"
? html`
${items.properties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + 1))}
${items.patternProperties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + 1))}
${items.additionalProperties ? this.renderAST(items.additionalProperties, "object", schemaLevel + 1, indentLevel + 1) : ""}
${items.unions?.map((u) => this.renderAST(u, "object", schemaLevel + 1, indentLevel + 1))}
`
: this.renderAST(items, "array", schemaLevel + 1, indentLevel + 1)
}
</div>
`
: ""
}
`;
}
// 2. Object kind
if (node.kind === "object") {
const typeLabel = node.dataTypeLabel || (node.nullable ? "object ┃ null" : "object");
const hasChildren = !!(
(node.properties && node.properties.length > 0) ||
(node.patternProperties && node.patternProperties.length > 0) ||
node.additionalProperties ||
(node.unions && node.unions.length > 0)
);
return html`
${
node.name
? html`
<div
class="tr ${isExpanded ? "expanded" : "collapsed"} object ${nullClass}"
data-obj="${node.name}"
title="${node.deprecated ? `Deprecated ${node.name}` : node.name}"
>
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="padding-left:${leftPadding}px">
${
hasChildren
? html`<button
type="button"
class="obj-toggle"
data-obj="${node.name}"
aria-label="Toggle ${node.name}"
aria-expanded="${isExpanded ? "true" : "false"}"
></button>`
: ""
}
<span
class="key-label"
title="${node.name || nothing}"
style="display:inline-block; ${hasChildren ? "margin-left:-6px;" : ""}"
>
${deprecatedIcon}${node.name}
</span>
${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}
</div>
<div class="td key-type" title="${readWriteTip || nothing}">${typeLabel}${readWriteBadge}</div>
${this.renderDescrCell(node.description)}
</div>
`
: ""
}
${
hasChildren
? html`
<div class="object-body">
${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)))}
${node.patternProperties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)))}
${node.additionalProperties ? this.renderAST(node.additionalProperties, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)) : ""}
${node.unions?.map((u) => this.renderAST(u, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)))}
</div>
`
: ""
}
`;
}
// 3. Union kind (oneOf / anyOf)
if (node.kind === "union") {
const opLabel =
(node.operator === "anyOf" ? "ANY OF" : "ONE OF") + (node.suffix ? ` ${node.suffix}` : "");
// Options are already labelled by their OPT row, so only property names are shown
const showName = node.name && parentType !== "xxx-of-option";
return html`
${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel, indentLevel))}
<div class="tr expanded xxx-of" data-obj="${showName ? node.name : opLabel}">
${
showName
? html`
<div class="td key" style="padding-left:${leftPadding}px">
<span class="key-label" title="${node.name}">${node.name}</span>
${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}
</div>
<div class="td key-type"><span class="xxx-of-key">${opLabel}</span></div>
`
: html`
<div class="td key" style="padding-left:${leftPadding}px">
<span class="xxx-of-key" style="margin-left:-6px">${opLabel}</span>
</div>
<div class="td key-type"></div>
`
}
${this.renderDescrCell(node.description)}
</div>
<div class="object-body">
${node.options?.map((opt, i) =>
opt.kind === "primitive"
? // A primitive option fits on a single row: the badge, then its type and description
html`<div class="object-body">
${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)}
</div>`
: html`
<div class="tr expanded xxx-of-option" data-obj="OPTION ${opt.optionIndex || i + 1}">
<div class="td key" style="padding-left:${leftPadding + 16}px">
<span class="xxx-of-key">OPT ${opt.optionIndex || i + 1}</span>
${
opt.optionTitle
? html`<span class="xxx-of-descr">${opt.optionTitle}</span>`
: ""
}
</div>
<div class="td key-type"></div>
<div class="td key-descr"></div>
</div>
<div class="object-body">
${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)}
</div>
`,
)}
</div>
`;
}
// 4. Primitive kind
if (node.kind === "primitive") {
const dataTypeCss = this.getDataTypeCss(node.type);
// Primitive option of a oneOf / anyOf: the OPT n badge (and the title) takes the place of the name
const isOption = parentType === "xxx-of-option" && !!node.optionIndex;
return html`
<div class="tr primitive ${nullClass}" title="${node.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="padding-left:${leftPadding}px">
${
isOption
? html`<span class="xxx-of-key">OPT ${node.optionIndex}</span
>${
node.optionTitle
? html`<span class="xxx-of-descr">${node.optionTitle}</span>`
: ""
}`
: ""
}${deprecatedIcon}${
node.name && !isOption
? html`<span class="key-label" title="${node.name}">${node.name}</span>`
: ""
}
${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}
</div>
<div class="td key-type ${dataTypeCss}" title="${readWriteTip || nothing}">
${parentType === "array" ? `[${node.type}]` : node.type}${readWriteBadge}
</div>
${this.renderDescrCell(node.description, node, node.title)}
</div>
`;
}
return "";
}
}