(function () { "use strict"; const SPEC_URL = window.__DOXA_API_SPEC_URL__ || "doxaApi.json"; let apis = []; let activeApiIndex = 0; let spec = null; let activeEndpoint = null; let activeTryTab = "body"; let activeCodeLang = "curl"; let mockMode = localStorage.getItem("DoxaApi.mockMode") === "1"; let collapsedGroups = new Set(JSON.parse(localStorage.getItem("DoxaApi.collapsed") || "[]")); // schemeId -> { value } | { username, password } | { token } let authCredentials = JSON.parse(localStorage.getItem("DoxaApi.auth") || "{}"); // Array of { id, method, path, status, elapsedMs, time, request, response } let requestHistory = JSON.parse(localStorage.getItem("DoxaApi.history") || "[]"); const el = { nav: document.getElementById("navContent"), detail: document.getElementById("detailContent"), try: document.getElementById("tryContent"), search: document.getElementById("searchInput"), themeToggle: document.getElementById("themeToggle"), brandTitle: document.getElementById("brandTitle"), brandVersion: document.getElementById("brandVersion"), apiSelector: document.getElementById("apiSelector"), modalRoot: document.getElementById("modalRoot"), }; function persistAuth() { localStorage.setItem("DoxaApi.auth", JSON.stringify(authCredentials)); updateAuthBadge(); } function persistHistory() { if (requestHistory.length > 30) requestHistory = requestHistory.slice(0, 30); localStorage.setItem("DoxaApi.history", JSON.stringify(requestHistory)); } function updateAuthBadge() { const dot = document.getElementById("authBadgeDot"); if (!dot) return; const hasAny = Object.keys(authCredentials).some((k) => credentialIsSet(authCredentials[k])); dot.style.display = hasAny ? "block" : "none"; } function credentialIsSet(cred) { if (!cred) return false; return !!(cred.value || cred.token || (cred.username && cred.password)); } function refreshApiSelector() { if (!el.apiSelector) return; el.apiSelector.innerHTML = apis.map((a, i) => ``).join(''); el.apiSelector.value = String(activeApiIndex); } async function init() { renderSkeleton(); try { const res = await fetch(SPEC_URL); if (!res.ok) throw new Error("HTTP " + res.status); const importedSpec = await res.json(); const extend = apis.length > 0 && window.confirm("Extend the currently selected API?\n\nOK = Extend Current API\nCancel = Import As New API (default)"); if (extend) { spec.groups = [...(spec.groups || []), ...(importedSpec.groups || [])]; spec.schemas = Object.assign(spec.schemas || {}, importedSpec.schemas || {}); spec.securitySchemes = Object.assign(spec.securitySchemes || {}, importedSpec.securitySchemes || {}); } else { apis.push(importedSpec); activeApiIndex = apis.length - 1; spec = importedSpec; } refreshApiSelector(); if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title; if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version; } catch (err) { el.nav.innerHTML = ``; return; } if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title; if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version; document.title = (spec.info && spec.info.title) || "API Documentation"; renderNav(""); renderOverview(); renderTryEmpty(); bindGlobalEvents(); updateAuthBadge(); } function renderSkeleton() { el.nav.innerHTML = Array.from({ length: 6 }) .map(() => `
`) .join(""); } function totalEndpointCount() { return (spec.groups || []).reduce((n, g) => n + g.endpoints.length, 0); } function totalSchemaCount() { return Object.keys(spec.schemas || {}).length; } function mockServerAvailable() { return !!(spec.features && spec.features.mockServer); } function consoleAvailable() { return !!(spec.features && spec.features.console); } // Nav rendering function renderNav(filterText) { const term = filterText.trim().toLowerCase(); const groups = spec.groups || []; let html = ``; if (groups.length === 0) { html += ``; el.nav.innerHTML = html; return; } let totalMatches = 0; let groupHtml = ""; for (const group of groups) { const endpoints = group.endpoints.filter((e) => matchesFilter(e, term)); if (term && endpoints.length === 0) continue; totalMatches += endpoints.length; const isCollapsed = collapsedGroups.has(group.name) && !term; groupHtml += ` `; } if (term && totalMatches === 0) { html += ``; } else { html += groupHtml; } el.nav.innerHTML = html; } function navEndpointHtml(group, endpoint) { const isActive = activeEndpoint && activeEndpoint.endpoint.operationId === endpoint.operationId; return ` `; } function matchesFilter(endpoint, term) { if (!term) return true; return ( endpoint.path.toLowerCase().includes(term) || (endpoint.summary || "").toLowerCase().includes(term) || endpoint.method.toLowerCase().includes(term) ); } // Overview / welcome screen function renderOverview() { const info = spec.info || {}; const groups = spec.groups || []; let html = `
`; html += `
`; html += `
API reference
`; html += `

${escapeHtml(info.title || "API Documentation")}

`; if (info.description) { html += `

${escapeHtml(info.description)}

`; } html += `
`; if (info.version) { html += ` ${escapeHtml(info.version)} `; } if (spec.servers && spec.servers.length) { html += ` ${escapeHtml(spec.servers[0])} `; } html += `
`; html += `
`; // hero html += `
`; html += `
${groups.length}
Groups
`; html += `
${totalEndpointCount()}
Endpoints
`; html += `
${totalSchemaCount()}
Schemas
`; html += `
${Object.keys(spec.securitySchemes || {}).length}
Auth schemes
`; html += `
`; if (spec.securitySchemes && Object.keys(spec.securitySchemes).length > 0) { html += `

Authentication

`; html += `
`; for (const [id, scheme] of Object.entries(spec.securitySchemes)) { html += ``; } html += `
`; } html += `

Browse by group

`; for (const group of groups) { html += `
${escapeHtml(group.name)} ${group.endpoints.length}
`; for (const ep of group.endpoints) { html += `
${ep.method} ${escapeHtml(ep.path)} ${escapeHtml(ep.summary || "")}
`; } html += `
`; } html += `
`; el.detail.innerHTML = html; el.detail.scrollTop = 0; } function renderTryEmpty() { el.try.innerHTML = `
Pick an endpoint to send a live request.
`; } function selectEndpoint(group, endpoint) { activeEndpoint = { group, endpoint }; activeTryTab = "body"; document.querySelectorAll(".nav-endpoint").forEach((b) => { b.classList.toggle("active", b.dataset.op === endpoint.operationId); }); const overviewLink = document.querySelector(".nav-overview-link"); if (overviewLink) overviewLink.classList.remove("active"); renderDetail(group, endpoint); renderTry(group, endpoint); } function renderDetail(group, endpoint) { const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`; let html = `
`; html += `
`; html += ``; html += `
${endpoint.method} ${escapeHtml(endpoint.path)}
`; html += `

${escapeHtml(endpoint.summary || endpoint.operationId)}

`; if (endpoint.description) { html += `

${escapeHtml(endpoint.description)}

`; } if (endpoint.deprecated) { html += `
Deprecated - this endpoint may be removed in a future version
`; } if (endpoint.security && endpoint.security.length > 0) { html += `
`; for (const schemeId of endpoint.security) { const scheme = (spec.securitySchemes || {})[schemeId]; html += ` ${escapeHtml(scheme ? authSchemeLabel(scheme) : schemeId)} `; } html += `
`; } html += `
`; if (endpoint.parameters && endpoint.parameters.length > 0) { html += `

Parameters ${endpoint.parameters.length}

`; html += ``; for (const p of endpoint.parameters) { html += ``; } html += `
NameLocated inTypeDescription
${escapeHtml(p.name)}${p.required ? '*' : ""} ${p.in} ${schemaTypeLabel(p.schema)} ${escapeHtml(p.description || "-")}
`; } if (endpoint.requestBody) { html += `

Request body

`; html += `
${renderSchemaTree(endpoint.requestBody.schema, 0)}
`; } if (endpoint.responses && endpoint.responses.length > 0) { html += `

Responses ${endpoint.responses.length}

`; for (const r of endpoint.responses) { const cls = r.statusCode[0] === "2" ? "status-2xx" : r.statusCode[0] === "4" ? "status-4xx" : "status-5xx"; html += `
${r.statusCode} ${escapeHtml(r.description || "")}
`; if (r.schema) { html += `
${renderSchemaTree(r.schema, 0)}
`; } html += `
`; } html += `
`; } html += `
`; el.detail.innerHTML = html; el.detail.scrollTop = 0; const copyBtn = document.getElementById("copyRouteBtn"); if (copyBtn) { copyBtn.addEventListener("click", () => { navigator.clipboard.writeText(endpoint.path).then(() => flashIcon(copyBtn)); }); } } function flashIcon(btn) { const original = btn.innerHTML; btn.innerHTML = ``; setTimeout(() => (btn.innerHTML = original), 1100); } function authSchemeLabel(scheme) { if (!scheme) return "Auth"; if (scheme.type === "apiKey") return `API key (${scheme.name || "key"})`; if (scheme.type === "http" && scheme.scheme === "bearer") return "Bearer token"; if (scheme.type === "http" && scheme.scheme === "basic") return "Basic auth"; if (scheme.type === "oauth2") return "OAuth 2.0"; return scheme.id || "Auth"; } function schemaTypeLabel(schema) { if (!schema) return "any"; if (schema.refName) return schema.refName; if (schema.type === "array") return schemaTypeLabel(schema.items) + "[]"; if (schema.type === "enum") return "enum"; return schema.format ? `${schema.type} (${schema.format})` : schema.type; } function renderSchemaTree(schema, depth) { if (!schema) return `unknown`; const indent = " ".repeat(depth); if (schema.refName && spec.schemas && spec.schemas[schema.refName] && depth < 6) { const resolved = spec.schemas[schema.refName]; return renderSchemaTree({ ...resolved, refName: undefined }, depth); } if (schema.type === "object" && schema.properties) { const required = new Set(schema.required || []); let lines = [`{`]; const entries = Object.entries(schema.properties); entries.forEach(([key, propSchema], i) => { const isReq = required.has(key); const comma = i < entries.length - 1 ? "," : ""; const nullableMark = propSchema && propSchema.nullable ? '?' : ""; lines.push( `${indent} ${escapeHtml(key)}${isReq ? '*' : ""}${nullableMark}: ${renderInlineType(propSchema, depth + 1)}${comma}` ); }); lines.push(`${indent}}`); return lines.join("\n"); } if (schema.type === "array") { return `[\n${indent} ${renderInlineType(schema.items, depth + 1)}\n${indent}]`; } if (schema.type === "enum") { return `enum (${(schema.enumValues || []).join(" | ")})`; } return `${schema.type}${schema.format ? " (" + schema.format + ")" : ""}`; } function renderInlineType(schema, depth) { if (!schema) return `any`; if (schema.refName) { if (spec.schemas && spec.schemas[schema.refName] && depth < 6) { return renderSchemaTree({ ...spec.schemas[schema.refName] }, depth); } return `${escapeHtml(schema.refName)}`; } if (schema.type === "object" && schema.properties) return renderSchemaTree(schema, depth); if (schema.type === "array") { return `Array<${renderInlineType(schema.items, depth)}>`; } if (schema.type === "enum") { return `(${(schema.enumValues || []).join(" | ")})`; } return `${schema.type}${schema.format ? " (" + schema.format + ")" : ""}`; } // Try-it-out pane function renderTry(group, endpoint) { const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`; let html = `
`; html += `
Try it
`; html += `
`; html += `
`; if (endpoint.security && endpoint.security.length > 0) { const satisfied = endpoint.security.some((id) => credentialIsSet(authCredentials[id])); html += `
${satisfied ? "Credentials will be sent with this request" : "This endpoint requires authorization"}
`; } if (mockServerAvailable()) { html += ``; } const pathParams = endpoint.parameters.filter((p) => p.in === "path"); const queryParams = endpoint.parameters.filter((p) => p.in === "query"); const headerParams = endpoint.parameters.filter((p) => p.in === "header"); if (pathParams.length) { html += `
Path parameters
`; for (const p of pathParams) { html += `
${escapeHtml(p.name)}${p.required ? '*' : ""}${schemaTypeLabel(p.schema)}
`; } html += `
`; } if (queryParams.length) { html += `
Query parameters
`; for (const p of queryParams) { html += `
${escapeHtml(p.name)}${p.required ? '*' : ""}${schemaTypeLabel(p.schema)}
`; } html += `
`; } if (headerParams.length) { html += `
Headers
`; for (const p of headerParams) { html += `
${escapeHtml(p.name)}${p.required ? '*' : ""}${schemaTypeLabel(p.schema)}
`; } html += `
`; } if (endpoint.requestBody) { const example = endpoint.requestBody.example || generateExampleJson(endpoint.requestBody.schema, 0); html += `
Request body (JSON)
`; } html += ``; html += `
`; html += `
`; // tryTabBody html += `
`; html += renderCodeTab(endpoint); html += `
`; html += `
`; html += renderHistoryTab(endpoint); html += `
`; html += `
`; el.try.innerHTML = html; document.getElementById("sendBtn").addEventListener("click", () => sendTryRequest(endpoint)); const mockToggle = document.getElementById("mockModeToggle"); if (mockToggle) { mockToggle.addEventListener("change", () => { mockMode = mockToggle.checked; localStorage.setItem("DoxaApi.mockMode", mockMode ? "1" : "0"); }); } const openAuthBtn = document.getElementById("openAuthFromTry"); if (openAuthBtn) { openAuthBtn.addEventListener("click", () => openAuthModal()); } el.try.querySelectorAll("[data-try-tab]").forEach((btn) => { btn.addEventListener("click", () => { activeTryTab = btn.dataset.tryTab; renderTry(group, endpoint); }); }); bindCodeTabEvents(endpoint); bindHistoryTabEvents(endpoint); } function renderCodeTab(endpoint) { const langs = [ { id: "curl", label: "cURL" }, { id: "js", label: "JavaScript" }, { id: "python", label: "Python" }, { id: "csharp", label: "C#" }, { id: "go", label: "Go" }, ]; let html = `
`; for (const lang of langs) { html += ``; } html += `
`; html += `
${langs.find((l) => l.id === activeCodeLang).label} snippet
`; html += `
${buildCodeSnippet(endpoint, activeCodeLang)}
`; return html; } function bindCodeTabEvents(endpoint) { const root = document.getElementById("tryTabCode"); if (!root) return; root.querySelectorAll("[data-code-lang]").forEach((btn) => { btn.addEventListener("click", () => { activeCodeLang = btn.dataset.codeLang; document.getElementById("tryTabCode").innerHTML = renderCodeTab(endpoint); bindCodeTabEvents(endpoint); }); }); const copyBtn = document.getElementById("copyCodeBtn"); if (copyBtn) { copyBtn.addEventListener("click", () => { const text = document.getElementById("codeSnippet").textContent; navigator.clipboard.writeText(text).then(() => { const original = copyBtn.innerHTML; copyBtn.textContent = "Copied"; setTimeout(() => (copyBtn.innerHTML = original), 1100); }); }); } } function renderHistoryTab(endpoint) { const items = requestHistory.filter((h) => h.operationId === endpoint.operationId); if (items.length === 0) { return `
No requests sent yet for this endpoint.
Responses you send will appear here.
`; } let html = ""; for (const item of items) { const cls = item.status && item.status < 300 ? "status-2xx" : item.status && item.status < 500 ? "status-4xx" : "status-5xx"; html += `
${item.status || "ERR"} ${escapeHtml(item.time)}
${escapeHtml(item.method)} ${escapeHtml(item.url)}
`; } return html; } function bindHistoryTabEvents(endpoint) { const root = document.getElementById("tryTabHistory"); if (!root) return; root.querySelectorAll("[data-history-id]").forEach((node) => { node.addEventListener("click", () => { const item = requestHistory.find((h) => h.id === node.dataset.historyId); if (!item) return; activeTryTab = "body"; renderTry({ name: "" }, endpoint); showResponsePanelFromHistory(item); }); }); } function showResponsePanelFromHistory(item) { const panel = document.getElementById("responsePanel"); if (!panel || !item.response) return; renderResponsePanel(panel, item.response); } // Builds a language-agnostic description of the request: url, headers, body. // Shared by every code-snippet generator and by the auth-aware "Send" flow. function buildRequestPlan(endpoint) { const base = (spec.servers && spec.servers[0]) || ""; const path = endpoint.path; const headers = { Accept: "application/json" }; if (endpoint.requestBody) { headers["Content-Type"] = endpoint.requestBody.contentType || "application/json"; } applyAuthHeaders(endpoint, headers); const body = endpoint.requestBody ? (endpoint.requestBody.example || generateExampleJson(endpoint.requestBody.schema, 0)) : null; return { url: base + path, method: endpoint.method, headers, body }; } // Mutates `headers` (and returns query/string additions where relevant) based on // whichever security schemes apply to the endpoint and have stored credentials. function applyAuthHeaders(endpoint, headers, queryParamsOut) { if (!endpoint.security || endpoint.security.length === 0) return; for (const schemeId of endpoint.security) { const scheme = (spec.securitySchemes || {})[schemeId]; const cred = authCredentials[schemeId]; if (!scheme || !credentialIsSet(cred)) continue; if (scheme.type === "apiKey") { const headerName = scheme.name || "X-API-Key"; if ((scheme.in || "header") === "header") { headers[headerName] = cred.value; } else if (scheme.in === "query" && queryParamsOut) { queryParamsOut[headerName] = cred.value; } } else if (scheme.type === "http" && scheme.scheme === "bearer") { headers["Authorization"] = `Bearer ${cred.token}`; } else if (scheme.type === "http" && scheme.scheme === "basic") { const encoded = typeof btoa === "function" ? btoa(`${cred.username}:${cred.password}`) : ""; headers["Authorization"] = `Basic ${encoded}`; } else if (scheme.type === "oauth2") { headers["Authorization"] = `Bearer ${cred.token}`; } // First matching satisfied scheme wins (schemes are an OR requirement). break; } } function buildCodeSnippet(endpoint, lang) { const plan = buildRequestPlan(endpoint); switch (lang) { case "js": return buildJsSnippet(plan); case "python": return buildPythonSnippet(plan); case "csharp": return buildCSharpSnippet(plan); case "go": return buildGoSnippet(plan); default: return buildCurlSnippet(plan); } } function buildCurlSnippet(plan) { const lines = []; lines.push(`curl -X ${plan.method} \\`); lines.push(` "${escapeHtml(plan.url)}" \\`); const headerEntries = Object.entries(plan.headers); headerEntries.forEach(([k, v], i) => { const isLast = i === headerEntries.length - 1 && !plan.body; lines.push(` -H "${escapeHtml(k)}: ${escapeHtml(v)}"${isLast ? "" : " \\"}`); }); if (plan.body) { lines.push(` -d '${escapeHtml(plan.body)}'`); } return lines.join("\n"); } function buildJsSnippet(plan) { const headersObj = JSON.stringify(plan.headers, null, 2).replace(/\n/g, "\n "); const lines = [ `const response = await fetch("${plan.url}", {`, ` method: "${plan.method}",`, ` headers: ${headersObj},`, ]; if (plan.body) { lines.push(` body: ${JSON.stringify(plan.body)},`); } lines.push(`});`); lines.push(`const data = await response.json();`); lines.push(`console.log(data);`); return escapeHtml(lines.join("\n")); } function buildPythonSnippet(plan) { const lines = [`import requests`, ``, `response = requests.request(`, ` "${plan.method}",`, ` "${plan.url}",`]; const headerLines = Object.entries(plan.headers).map(([k, v]) => ` "${k}": "${v}",`); lines.push(` headers={`); lines.push(...headerLines); lines.push(` },`); if (plan.body) { lines.push(` data="""${plan.body}""",`); } lines.push(`)`); lines.push(``); lines.push(`print(response.status_code, response.json())`); return escapeHtml(lines.join("\n")); } function buildCSharpSnippet(plan) { const lines = [ `using var client = new HttpClient();`, `using var request = new HttpRequestMessage(HttpMethod.${capitalize(plan.method.toLowerCase())}, "${plan.url}");`, ]; for (const [k, v] of Object.entries(plan.headers)) { if (k.toLowerCase() === "content-type") continue; lines.push(`request.Headers.TryAddWithoutValidation("${k}", "${v}");`); } if (plan.body) { const contentType = plan.headers["Content-Type"] || "application/json"; lines.push(`request.Content = new StringContent(${JSON.stringify(plan.body)}, System.Text.Encoding.UTF8, "${contentType}");`); } lines.push(`using var response = await client.SendAsync(request);`); lines.push(`var body = await response.Content.ReadAsStringAsync();`); lines.push(`Console.WriteLine($"{(int)response.StatusCode}: {body}");`); return escapeHtml(lines.join("\n")); } function buildGoSnippet(plan) { const lines = [ `package main`, ``, `import (`, `\t"fmt"`, `\t"io"`, `\t"net/http"`, `\t"strings"`, `)`, ``, `func main() {`, ]; if (plan.body) { lines.push(`\tbody := strings.NewReader(\`${plan.body.replace(/`/g, "'")}\`)`); lines.push(`\treq, _ := http.NewRequest("${plan.method}", "${plan.url}", body)`); } else { lines.push(`\treq, _ := http.NewRequest("${plan.method}", "${plan.url}", nil)`); } for (const [k, v] of Object.entries(plan.headers)) { lines.push(`\treq.Header.Set("${k}", "${v}")`); } lines.push(`\tresp, err := http.DefaultClient.Do(req)`); lines.push(`\tif err != nil {`); lines.push(`\t\tpanic(err)`); lines.push(`\t}`); lines.push(`\tdefer resp.Body.Close()`); lines.push(`\tdata, _ := io.ReadAll(resp.Body)`); lines.push(`\tfmt.Println(resp.StatusCode, string(data))`); lines.push(`}`); return escapeHtml(lines.join("\n")); } function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } function generateExampleJson(schema, depth) { const value = generateExampleValue(schema, depth, new Set()); return JSON.stringify(value, null, 2); } function generateExampleValue(schema, depth, seen) { if (!schema || depth > 6) return null; if (schema.refName) { if (seen.has(schema.refName)) return {}; const resolved = spec.schemas && spec.schemas[schema.refName]; if (!resolved) return {}; const nextSeen = new Set(seen); nextSeen.add(schema.refName); return generateExampleValue(resolved, depth + 1, nextSeen); } switch (schema.type) { case "string": if (schema.format === "date-time") return new Date().toISOString(); if (schema.format === "uuid") return "00000000-0000-0000-0000-000000000000"; return "string"; case "integer": return 0; case "number": return 0; case "boolean": return true; case "enum": return (schema.enumValues && schema.enumValues[0]) || "string"; case "array": return [generateExampleValue(schema.items, depth + 1, seen)]; case "object": { if (!schema.properties) return {}; const obj = {}; for (const [key, propSchema] of Object.entries(schema.properties)) { obj[key] = generateExampleValue(propSchema, depth + 1, seen); } return obj; } default: return null; } } async function sendTryRequest(endpoint) { const btn = document.getElementById("sendBtn"); const label = document.getElementById("sendBtnLabel"); const panel = document.getElementById("responsePanel"); let path = endpoint.path; document.querySelectorAll('[data-param-in="path"]').forEach((input) => { const name = input.dataset.paramName; path = path.replace(`{${name}}`, encodeURIComponent(input.value || "")); }); const isMock = mockMode && mockServerAvailable(); const url = isMock ? new URL("mock", window.location.href) : new URL(path, window.location.origin); if (isMock) { url.searchParams.set("operationId", endpoint.operationId); } document.querySelectorAll('[data-param-in="query"]').forEach((input) => { if (input.value) url.searchParams.set(input.dataset.paramName, input.value); }); const headers = { Accept: "application/json" }; document.querySelectorAll('[data-param-in="header"]').forEach((input) => { if (input.value) headers[input.dataset.paramName] = input.value; }); let body = undefined; if (endpoint.requestBody) { headers["Content-Type"] = endpoint.requestBody.contentType || "application/json"; const bodyEl = document.getElementById("tryBody"); body = bodyEl ? bodyEl.value : undefined; } const authQueryParams = {}; if (!isMock) { applyAuthHeaders(endpoint, headers, authQueryParams); for (const [k, v] of Object.entries(authQueryParams)) { url.searchParams.set(k, v); } } btn.disabled = true; label.textContent = "Sending…"; btn.querySelector("svg").style.display = "none"; btn.insertBefore(spinnerEl(), btn.firstChild); const startTime = performance.now(); let resultForHistory = null; try { const res = await fetch(url.toString(), { method: isMock ? "GET" : endpoint.method, headers: isMock ? { Accept: "application/json" } : headers, body: isMock || endpoint.method === "GET" || endpoint.method === "HEAD" ? undefined : body, }); const elapsedMs = Math.round(performance.now() - startTime); const contentType = res.headers.get("content-type") || ""; let bodyText; let isJson = false; if (contentType.includes("application/json")) { try { const json = await res.json(); bodyText = JSON.stringify(json, null, 2); isJson = true; } catch { bodyText = await res.text(); } } else { bodyText = await res.text(); } resultForHistory = { status: res.status, ok: res.ok, elapsedMs, body: bodyText, isJson, isMock, }; renderResponsePanel(panel, resultForHistory); } catch (err) { const elapsedMs = Math.round(performance.now() - startTime); resultForHistory = { status: null, ok: false, elapsedMs, body: String(err && err.message ? err.message : err), isJson: false, networkError: true, }; renderResponsePanel(panel, resultForHistory); } finally { btn.disabled = false; label.textContent = "Send request"; const spinner = btn.querySelector(".spinner"); if (spinner) spinner.remove(); btn.querySelector("svg").style.display = ""; requestHistory.unshift({ id: "h" + Date.now() + Math.random().toString(36).slice(2, 7), operationId: endpoint.operationId, method: isMock ? "GET (mock)" : endpoint.method, url: url.toString(), status: resultForHistory ? resultForHistory.status : null, time: new Date().toLocaleTimeString(), response: resultForHistory, }); persistHistory(); } } function spinnerEl() { const s = document.createElement("span"); s.className = "spinner"; return s; } function renderResponsePanel(panel, result) { const statusClass = result.networkError ? "status-5xx" : result.status < 300 ? "status-2xx" : result.status < 500 ? "status-4xx" : "status-5xx"; const statusLabel = result.networkError ? "Network error" : result.status; panel.innerHTML = `
${statusLabel} ${result.isMock ? ` Mock ` : ""} ${result.elapsedMs} ms
${result.isJson ? syntaxHighlightJson(result.body) : escapeHtml(result.body) }
`; document.getElementById("copyResponseBtn").addEventListener("click", () => { navigator.clipboard.writeText(result.body).then(() => { const btn = document.getElementById("copyResponseBtn"); btn.lastChild.textContent = "Copied"; setTimeout(() => (btn.lastChild.textContent = "Copy"), 1200); }); }); } function syntaxHighlightJson(json) { const escaped = escapeHtml(json); return escaped.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(\.\d+)?([eE][+-]?\d+)?)/g, (match) => { let cls = "json-number"; if (/^"/.test(match)) { cls = /:$/.test(match) ? "json-key" : "json-string"; } else if (/true|false/.test(match)) { cls = "json-boolean"; } else if (/null/.test(match)) { cls = "json-null"; } return `${match}`; } ); } // --------------------------------------------------------------- // Modal system - lightweight, dependency-free, used for Auth, // Console, and Diff. Only one modal is shown at a time. // --------------------------------------------------------------- function closeModal() { if (!el.modalRoot) return; el.modalRoot.innerHTML = ""; } function openModal(title, bodyHtml, { wide } = {}) { if (!el.modalRoot) return null; el.modalRoot.innerHTML = ` `; const backdrop = document.getElementById("modalBackdrop"); document.getElementById("modalCloseBtn").addEventListener("click", closeModal); backdrop.addEventListener("click", (e) => { if (e.target === backdrop) closeModal(); }); const escHandler = (e) => { if (e.key === "Escape") { closeModal(); document.removeEventListener("keydown", escHandler); } }; document.addEventListener("keydown", escHandler); return document.getElementById("modalBody"); } // --------------------------------------------------------------- // Authorization modal - one card per security scheme declared by // the API, with scheme-appropriate inputs (API key / bearer token / // basic credentials / OAuth2 client credentials). Values are kept // in localStorage so they survive reloads, and are applied to every // "Try it" request, cURL snippet, and generated code sample whose // endpoint references that scheme. // --------------------------------------------------------------- function openAuthModal() { const schemes = (spec && spec.securitySchemes) || {}; const ids = Object.keys(schemes); let body; if (ids.length === 0) { body = `
This API doesn't declare any security schemes.
Configure them via DoxaApiOptions.AddBearerAuth() and similar helpers.
`; } else { body = ids.map((id) => authSchemeCardHtml(id, schemes[id])).join(""); } const modalBody = openModal("Authorization", body, {}); if (!modalBody || ids.length === 0) return; for (const id of ids) { bindAuthSchemeCard(modalBody, id, schemes[id]); } } function authSchemeCardHtml(id, scheme) { const cred = authCredentials[id] || {}; const isSet = credentialIsSet(cred); let fieldsHtml = ""; if (scheme.type === "apiKey") { fieldsHtml = `
${escapeHtml(scheme.name || "API key")}${escapeHtml(scheme.in || "header")}
`; } else if (scheme.type === "http" && scheme.scheme === "basic") { fieldsHtml = `
Username
Password
`; } else if (scheme.type === "oauth2") { const flow = scheme.flows && (scheme.flows.clientCredentials || scheme.flows.authorizationCode); fieldsHtml = `
Access tokenpaste a token, or fetch one separately
${flow && flow.tokenUrl ? `
Token URL: ${escapeHtml(flow.tokenUrl)}
` : ""}`; } else { // http bearer (default) and any unrecognized http-style scheme fieldsHtml = `
Bearer token${scheme.bearerFormat ? ` ${escapeHtml(scheme.bearerFormat)}` : ""}
`; } return `
${escapeHtml(authSchemeLabel(scheme))} ${escapeHtml(scheme.type)}
${scheme.description ? `

${escapeHtml(scheme.description)}

` : ""} ${fieldsHtml}
`; } function bindAuthSchemeCard(root, id, scheme) { const card = root.querySelector(`[data-scheme-id="${cssEscape(id)}"]`); if (!card) return; const persistFromInputs = () => { const cred = {}; card.querySelectorAll("[data-auth-field]").forEach((input) => { cred[input.dataset.authField] = input.value; }); authCredentials[id] = cred; persistAuth(); const statusDot = card.querySelector(".auth-scheme-status"); if (statusDot) statusDot.classList.toggle("set", credentialIsSet(cred)); }; card.querySelectorAll("[data-auth-field]").forEach((input) => { input.addEventListener("input", persistFromInputs); }); const clearBtn = card.querySelector("[data-auth-clear]"); if (clearBtn) { clearBtn.addEventListener("click", () => { delete authCredentials[id]; persistAuth(); card.querySelectorAll("[data-auth-field]").forEach((input) => (input.value = "")); const statusDot = card.querySelector(".auth-scheme-status"); if (statusDot) statusDot.classList.remove("set"); }); } } function cssEscape(s) { return String(s).replace(/["\\]/g, "\\$&"); } // --------------------------------------------------------------- // Console - a free-form HTTP request runner that isn't tied to any // documented operation. Requests are sent server-side via the // /{prefix}/console endpoint, which avoids CORS restrictions when // pointing at a different host than the one serving the docs. // --------------------------------------------------------------- function openConsoleModal() { const base = (spec.servers && spec.servers[0]) || ""; const body = `
Headers (one per line, "Name: value")
Body
`; const modalBody = openModal("API Console", body, { wide: true }); if (!modalBody) return; document.getElementById("consoleSendBtn").addEventListener("click", sendConsoleRequest); } function parseHeaderLines(text) { const headers = {}; (text || "").split("\n").forEach((line) => { const idx = line.indexOf(":"); if (idx === -1) return; const key = line.slice(0, idx).trim(); const value = line.slice(idx + 1).trim(); if (key) headers[key] = value; }); return headers; } async function sendConsoleRequest() { const btn = document.getElementById("consoleSendBtn"); const label = document.getElementById("consoleSendLabel"); const panel = document.getElementById("consoleResponsePanel"); const method = document.getElementById("consoleMethod").value; const url = document.getElementById("consoleUrl").value.trim(); const headers = parseHeaderLines(document.getElementById("consoleHeaders").value); const body = document.getElementById("consoleBody").value; if (!url) { panel.innerHTML = `
Enter a URL to send a request.
`; return; } btn.disabled = true; label.textContent = "Sending…"; const startTime = performance.now(); try { const res = await fetch("console", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ method, url, headers, body: body || undefined }), }); const payload = await res.json(); const elapsedMs = payload.elapsedMs || Math.round(performance.now() - startTime); let isJson = false; let bodyText = payload.body || ""; try { JSON.parse(bodyText); isJson = true; } catch { isJson = false; } renderResponsePanel(panel, { status: payload.statusCode, ok: payload.statusCode >= 200 && payload.statusCode < 300, elapsedMs, body: isJson ? JSON.stringify(JSON.parse(bodyText), null, 2) : bodyText, isJson, networkError: !!payload.networkError, }); } catch (err) { renderResponsePanel(panel, { status: null, ok: false, elapsedMs: Math.round(performance.now() - startTime), body: String(err && err.message ? err.message : err), isJson: false, networkError: true, }); } finally { btn.disabled = false; label.textContent = "Send request"; } } // --------------------------------------------------------------- // Diff - compares the currently loaded spec against an uploaded // OpenAPI / Swagger / DoxaApi JSON file and reports added, removed, // and changed operations - useful for spotting breaking changes // before shipping a new API version. // --------------------------------------------------------------- function computeSpecDiff(oldSpec, newSpec) { const key = (ep) => `${ep.method} ${ep.path}`; const oldOps = new Map(); const newOps = new Map(); for (const g of oldSpec.groups || []) for (const e of g.endpoints) oldOps.set(key(e), e); for (const g of newSpec.groups || []) for (const e of g.endpoints) newOps.set(key(e), e); const rows = []; for (const [k, ep] of newOps) { if (!oldOps.has(k)) rows.push({ tag: "added", key: k, summary: ep.summary }); } for (const [k, ep] of oldOps) { if (!newOps.has(k)) rows.push({ tag: "removed", key: k, summary: ep.summary }); } for (const [k, oldEp] of oldOps) { const newEp = newOps.get(k); if (!newEp) continue; const changes = []; if ((oldEp.requestBody == null) !== (newEp.requestBody == null)) changes.push("request body"); if (JSON.stringify((oldEp.parameters || []).map((p) => p.name).sort()) !== JSON.stringify((newEp.parameters || []).map((p) => p.name).sort())) changes.push("parameters"); if (JSON.stringify((oldEp.security || []).slice().sort()) !== JSON.stringify((newEp.security || []).slice().sort())) changes.push("security"); if (!!oldEp.deprecated !== !!newEp.deprecated) changes.push("deprecation status"); if (changes.length > 0) { rows.push({ tag: "changed", key: k, summary: changes.join(", ") }); } } return rows.sort((a, b) => a.key.localeCompare(b.key)); } function openDiffModal(otherSpec) { const rows = computeSpecDiff(spec, otherSpec); const added = rows.filter((r) => r.tag === "added").length; const removed = rows.filter((r) => r.tag === "removed").length; const changed = rows.filter((r) => r.tag === "changed").length; let body = `
+${added} endpoints added
-${removed} endpoints removed
~${changed} endpoints changed
`; if (rows.length === 0) { body += `
No differences found between the current API and the uploaded spec.
`; } else { body += rows.map((r) => `
${r.tag} ${escapeHtml(r.key)}${r.summary ? ` - ${escapeHtml(r.summary)}` : ""}
`).join(""); } openModal("Compare specs", body, { wide: true }); } // Global events function bindGlobalEvents() { const importBtn = document.getElementById("importBtn"); const importFile = document.getElementById("importFile"); const exportToggle = document.querySelector(".export-toggle"); const exportDropdown = document.querySelector(".export-dropdown"); const exportMenu = document.getElementById("exportMenu"); const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn"); const exportOpenApiBtn = document.getElementById("exportOpenApiBtn"); const exportSwaggerBtn = document.getElementById("exportSwaggerBtn"); exportToggle?.addEventListener("click", (event) => { event.stopPropagation(); const isOpen = exportMenu?.classList.toggle("open"); exportToggle.setAttribute("aria-expanded", String(!!isOpen)); }); document.addEventListener("click", (event) => { if (!exportDropdown?.contains(event.target)) { exportMenu?.classList.remove("open"); exportToggle?.setAttribute("aria-expanded", "false"); } }); const dashboardBtn = document.getElementById("dashboardBtn"); const closeDashboard = document.getElementById("closeDashboard"); const dashboardPanel = document.getElementById("dashboardPanel"); importBtn?.addEventListener("click", () => importFile.click()); importFile?.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; const text = await file.text(); const res = await fetch(new URL("import", document.baseURI), { method: "POST", headers: { "Content-Type": "application/json" }, body: text }); if (!res.ok) { alert(await res.text()); return; } const importedSpec = await res.json(); const extend = apis.length > 0 && window.confirm("Extend the currently selected API?\n\nOK = Extend Current API\nCancel = Import As New API (default)"); if (extend) { spec.groups = [...(spec.groups || []), ...(importedSpec.groups || [])]; spec.schemas = Object.assign(spec.schemas || {}, importedSpec.schemas || {}); spec.securitySchemes = Object.assign(spec.securitySchemes || {}, importedSpec.securitySchemes || {}); } else { apis.push(importedSpec); activeApiIndex = apis.length - 1; spec = importedSpec; } refreshApiSelector(); if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title; if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version; activeEndpoint = null; renderNav(""); renderOverview(); renderTryEmpty(); }); function download(url, filename) { const a = document.createElement("a"); a.href = new URL(url, document.baseURI); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } exportDoxaApiBtn?.addEventListener("click", () => download("doxaApi.json", "DoxaApi.json")); exportOpenApiBtn?.addEventListener("click", () => download("openapi.json", "openapi.json")); exportSwaggerBtn?.addEventListener("click", () => download("swagger.json", "swagger.json")); // Dashboard functionality dashboardBtn?.addEventListener("click", () => openDashboard()); closeDashboard?.addEventListener("click", () => closeDashboardPanel()); const authBtn = document.getElementById("authBtn"); authBtn?.addEventListener("click", () => openAuthModal()); const consoleBtn = document.getElementById("consoleBtn"); if (consoleBtn) { if (!consoleAvailable()) { consoleBtn.style.display = "none"; } else { consoleBtn.addEventListener("click", () => openConsoleModal()); } } const diffFile = document.getElementById("diffFile"); const compareBtn = document.getElementById("compareBtn"); compareBtn?.addEventListener("click", () => diffFile?.click()); diffFile?.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; try { const text = await file.text(); const parsed = JSON.parse(text); const normalized = parsed.groups ? parsed : await (async () => { const res = await fetch(new URL("import", document.baseURI), { method: "POST", headers: { "Content-Type": "application/json" }, body: text }); if (!res.ok) throw new Error(await res.text()); return res.json(); })(); openDiffModal(normalized); } catch (err) { alert("Couldn't compare spec: " + (err && err.message ? err.message : err)); } finally { diffFile.value = ""; } }); el.nav.addEventListener("click", (e) => { const overviewBtn = e.target.closest("[data-overview]"); if (overviewBtn) { activeEndpoint = null; document.querySelectorAll(".nav-endpoint").forEach((b) => b.classList.remove("active")); overviewBtn.classList.add("active"); renderOverview(); renderTryEmpty(); return; } const groupToggle = e.target.closest("[data-toggle-group]"); if (groupToggle) { const name = groupToggle.dataset.toggleGroup; if (collapsedGroups.has(name)) collapsedGroups.delete(name); else collapsedGroups.add(name); localStorage.setItem("DoxaApi.collapsed", JSON.stringify([...collapsedGroups])); renderNav(el.search.value); return; } const endpointBtn = e.target.closest("[data-op]"); if (endpointBtn) { const opId = endpointBtn.dataset.op; for (const group of spec.groups) { const endpoint = group.endpoints.find((ep) => ep.operationId === opId); if (endpoint) { selectEndpoint(group, endpoint); break; } } } }); el.detail.addEventListener("click", (e) => { const authBadge = e.target.closest("[data-overview-auth]"); if (authBadge) { openAuthModal(); return; } const row = e.target.closest("[data-op]"); if (!row) return; const opId = row.dataset.op; for (const group of spec.groups) { const endpoint = group.endpoints.find((ep) => ep.operationId === opId); if (endpoint) { selectEndpoint(group, endpoint); break; } } }); el.search.addEventListener("input", () => renderNav(el.search.value)); document.addEventListener("keydown", (e) => { if (e.key === "/" && document.activeElement !== el.search) { e.preventDefault(); el.search.focus(); } if (e.key === "Escape" && document.activeElement === el.search) { el.search.blur(); } }); el.themeToggle.addEventListener("click", () => { const root = document.documentElement; const current = root.getAttribute("data-theme"); const isDark = current === "dark" || (current === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches); const next = isDark ? "light" : "dark"; root.setAttribute("data-theme", next); localStorage.setItem("DoxaApi.theme", next); }); const savedTheme = localStorage.getItem("DoxaApi.theme"); if (savedTheme) document.documentElement.setAttribute("data-theme", savedTheme); } // Helpers function escapeHtml(str) { return String(str ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[c])); } function escapeAttr(str) { return escapeHtml(str); } // Dashboard functions async function openDashboard() { const dashboardPanel = document.getElementById("dashboardPanel"); const dashboard = document.querySelector(".dashboard-panel"); if (!dashboardPanel) return; dashboardPanel.style.display = "block"; try { const res = await fetch(new URL("analytics", document.baseURI)); if (!res.ok) throw new Error("Failed to load analytics"); const analyticsData = await res.json(); updateDashboard(analyticsData); // Refresh every 5 seconds if (window.dashboardInterval) clearInterval(window.dashboardInterval); window.dashboardInterval = setInterval(async () => { const res = await fetch(new URL("analytics", document.baseURI)); if (res.ok) { const data = await res.json(); updateDashboard(data); } }, 5000); } catch (err) { console.error("Dashboard load error:", err); } } function closeDashboardPanel() { const dashboardPanel = document.getElementById("dashboardPanel"); if (dashboardPanel) dashboardPanel.style.display = "none"; if (window.dashboardInterval) { clearInterval(window.dashboardInterval); window.dashboardInterval = null; } } function updateDashboard(data) { document.getElementById("totalEndpoints").textContent = data.totalEndpoints || "0"; document.getElementById("totalRequests").textContent = data.totalRequests || "0"; document.getElementById("totalErrors").textContent = data.totalErrors || "0"; document.getElementById("avgResponse").textContent = Math.round((data.averageResponseTime || 0)) + "ms"; // Update top endpoints const topEndpointsList = (data.topEndpoints || []).map(ep => `
${ep.method} ${ep.path}
${ep.requestCount} req
`).join("") || "
No data yet
"; document.getElementById("topEndpointsChart").innerHTML = `

Top Endpoints

${topEndpointsList}
`; // Update recent requests const recentRequestsList = (data.recentRequests || []).map(req => { const statusClass = req.statusCode >= 400 ? "request-status-error" : "request-status-success"; return `
${req.method} ${req.path}
${req.statusCode} ${req.responseTimeMs}ms
`; }).join("") || "
No requests yet
"; document.getElementById("recentRequestsTable").innerHTML = `

Recent Requests

${recentRequestsList}
`; } /** * Enhanced Try Pane - Advanced Request Builder * Integrates RequestBuilder and CodeGenerator modules */ function renderAdvancedTryPane(group, endpoint) { if (!endpoint) return renderTryEmpty(); const paramState = new RequestBuilder.ParameterState(); RequestBuilder.populateDefaults(endpoint, paramState); let html = `
${escapeHtml(endpoint.method)} ${escapeHtml(endpoint.path)}
${endpoint.deprecated ? 'DEPRECATED' : ''}
`; // Path parameters if (endpoint.parameters?.filter(p => p.in === 'path').length > 0) { html += renderParamGroup('Path Parameters', endpoint.parameters.filter(p => p.in === 'path'), paramState); } // Query parameters if (endpoint.parameters?.filter(p => p.in === 'query').length > 0) { html += renderParamGroup('Query Parameters', endpoint.parameters.filter(p => p.in === 'query'), paramState); } // Headers if (endpoint.parameters?.filter(p => p.in === 'header').length > 0) { html += renderParamGroup('Headers', endpoint.parameters.filter(p => p.in === 'header'), paramState); } // Request body if (endpoint.requestBody) { html += `
Request Body
${endpoint.requestBody.example ? JSON.stringify(endpoint.requestBody.example, null, 2) : '{}'}
`; } html += `
`; // Code Generator html += `
Generate Code
`; CodeGenerator.getSupportedLanguages().forEach(lang => { html += ``; }); html += `
// Select a language above
`; // Response preview area html += ` `; html += `
`; el.try.innerHTML = html; // Bind code generator document.querySelectorAll('.code-lang-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.code-lang-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); activeCodeLang = btn.dataset.lang; updateCodePreview(endpoint, paramState); }); }); // Initial code preview updateCodePreview(endpoint, paramState); // Copy code button document.getElementById('codeCopyBtn')?.addEventListener('click', () => { const code = document.getElementById('codeBlock').textContent; navigator.clipboard.writeText(code).then(() => { const btn = document.getElementById('codeCopyBtn'); btn.classList.add('copied'); btn.textContent = 'Copied!'; setTimeout(() => { btn.classList.remove('copied'); btn.textContent = 'Copy Code'; }, 2000); }); }); } /** * Render parameter group */ function renderParamGroup(name, parameters, paramState) { if (!parameters.length) return ''; const paramIn = parameters[0].in; let html = `
${name}
`; parameters.forEach(param => { html += `
${escapeHtml(param.name)}
${param.schema?.type || 'string'}
`; }); html += `
`; return html; } /** * Update code preview based on current parameters */ function updateCodePreview(endpoint, paramState) { const currentLangBtn = document.querySelector('.code-lang-btn.active'); const lang = currentLangBtn?.dataset.lang || 'curl'; // Gather current param values from UI document.querySelectorAll('.param-input').forEach(input => { const paramIn = input.dataset.paramIn; const paramName = input.dataset.paramName; if (paramIn && paramName) { paramState[paramIn][paramName] = input.value; } }); // Get request body if present const bodyInput = document.querySelector('[data-request-body="true"]'); if (bodyInput) { try { paramState.body = JSON.parse(bodyInput.textContent); } catch (e) { paramState.body = bodyInput.textContent; } } const request = RequestBuilder.buildRequest(endpoint, paramState, window.location.origin); const code = CodeGenerator.generate(lang, request); document.getElementById('codeBlock').textContent = code; } /** * Integration point for workspace manager */ function initializeWorkspaceManager() { // Load initial workspaces const workspaces = WorkspaceManager.getWorkspaces(); if (workspaces.length === 0) { const defaultWs = new WorkspaceManager.Workspace('My Workspace'); const defaultCol = new WorkspaceManager.Collection('API Collection'); defaultWs.collections.push(defaultCol); WorkspaceManager.saveWorkspace(defaultWs); } // Setup environment selector if environments panel exists const envList = document.querySelector('.env-list'); if (envList) { const environments = WorkspaceManager.getEnvironments(); if (environments.length === 0) { const defaultEnv = new WorkspaceManager.Environment('Default Environment'); WorkspaceManager.saveEnvironment(defaultEnv); } } } /** * Save current request to collection */ function saveCurrentRequest() { if (!activeEndpoint || !spec) return; const name = prompt('Request name:', `${activeEndpoint.method} ${activeEndpoint.path}`); if (!name) return; const workspace = WorkspaceManager.getCurrentWorkspace(); const collection = workspace.collections[0]; if (!collection) return; const saved = new WorkspaceManager.SavedRequest( name, activeEndpoint.method, activeEndpoint.path ); saved.description = activeEndpoint.description || ''; // Gather current parameters document.querySelectorAll('.param-input').forEach(input => { const paramIn = input.dataset.paramIn; const paramName = input.dataset.paramName; if (paramIn && paramName && input.value) { saved.params[paramIn][paramName] = input.value; } }); // Save body const bodyInput = document.querySelector('[data-request-body="true"]'); if (bodyInput) { saved.body = bodyInput.textContent; } WorkspaceManager.saveRequestToCollection(workspace.id, collection.id, saved); alert(`Request saved to "${collection.name}"`); } /** * Keyboard shortcuts and accessibility */ function initializeKeyboardShortcuts() { const shortcuts = { '/': () => document.getElementById('searchInput')?.focus(), 'KeyS': (e) => e.ctrlKey && saveCurrentRequest(), 'KeyK': (e) => e.ctrlKey && toggleCommandPalette(), 'Tab': (e) => handleTabNavigation(e), 'Escape': () => closeModals() }; document.addEventListener('keydown', (e) => { // Don't trigger shortcuts when typing in input fields if (e.target.matches('input, textarea, [contenteditable]')) { if (e.key === 'Escape') { e.target.blur(); } return; } const handler = shortcuts[e.code] || shortcuts[e.key]; if (handler) { handler(e); } }); // Show keyboard shortcut hints addShortcutHints(); } /** * Add visual keyboard shortcut hints to UI */ function addShortcutHints() { const searchInput = document.getElementById('searchInput'); if (searchInput && !searchInput.parentElement.querySelector('.kbd')) { const kbd = document.createElement('kbd'); kbd.className = 'kbd search-kbd'; kbd.textContent = '/'; searchInput.parentElement.appendChild(kbd); } } /** * Toggle command palette (future enhancement) */ function toggleCommandPalette() { // Placeholder for future command palette implementation console.log('Command palette not yet implemented'); } /** * Handle Tab navigation for better accessibility */ function handleTabNavigation(e) { const focusableElements = document.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return; const currentIndex = Array.from(focusableElements).indexOf(document.activeElement); let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1; if (nextIndex >= focusableElements.length) nextIndex = 0; if (nextIndex < 0) nextIndex = focusableElements.length - 1; const nextElement = focusableElements[nextIndex]; if (nextElement) { e.preventDefault(); nextElement.focus(); } } /** * Close all open modals */ function closeModals() { document.querySelectorAll('[role="dialog"], .modal, .drawer').forEach(modal => { modal.style.display = 'none'; }); } /** * Enhance accessibility with ARIA labels */ function enhanceAccessibility() { // Add ARIA roles and labels const sidebar = document.getElementById('sidebar'); if (sidebar) sidebar.setAttribute('role', 'navigation'); const detailPane = document.getElementById('detailPane'); if (detailPane) detailPane.setAttribute('role', 'main'); const tryPane = document.getElementById('tryPane'); if (tryPane) tryPane.setAttribute('role', 'complementary'); // Add aria-labels to icon buttons document.querySelectorAll('.icon-btn').forEach(btn => { if (!btn.getAttribute('aria-label')) { btn.setAttribute('aria-label', btn.title || btn.textContent); } }); // Add roles to custom components document.querySelectorAll('.param-group-header').forEach(header => { header.setAttribute('role', 'button'); header.setAttribute('tabindex', '0'); }); document.querySelectorAll('[data-overview], [data-endpoint]').forEach(link => { link.setAttribute('role', 'menuitem'); }); } /** * Add focus visible styles for keyboard navigation */ function initializeFocusStyles() { const style = document.createElement('style'); style.textContent = ` :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } button:focus-visible, a:focus-visible, input:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } `; document.head.appendChild(style); } /** * Enhanced error handling and user feedback */ function showNotification(message, type = 'info', duration = 3000) { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.setAttribute('role', 'status'); notification.setAttribute('aria-live', 'polite'); notification.textContent = message; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; padding: 12px 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-md); color: var(--text-1); z-index: 9999; animation: slideIn 0.3s ease-out; `; document.body.appendChild(notification); if (duration > 0) { setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => notification.remove(), 300); }, duration); } return notification; } /** * Improved error logging */ const originalConsoleError = console.error; console.error = function(...args) { originalConsoleError.apply(console, args); const message = args[0]?.toString?.() || String(args[0]); if (message && !message.includes('HTTP')) { showNotification(`Error: ${message.substring(0, 50)}`, 'error', 5000); } }; /** * Initialize all enhancements */ function initializeEnhancements() { initializeWorkspaceManager(); initializeKeyboardShortcuts(); enhanceAccessibility(); initializeFocusStyles(); } // Call on load initializeEnhancements(); init(); })();