UNPKG

@scalar/api-reference

Version:

Generate beautiful API references from OpenAPI documents

678 lines (677 loc) 28.5 kB
import { defineComponent, computed, ref, watch, useId, provide, onServerPrefetch, onBeforeMount, useTemplateRef, onMounted, onBeforeUnmount, createElementBlock, openBlock, createBlock, createElementVNode, createVNode, resolveDynamicComponent, withCtx, createTextVNode, toDisplayString, normalizeClass, createCommentVNode, unref, renderSlot, normalizeProps, guardReactiveProps, createSlots } from "vue"; import { provideUseId } from "@headlessui/vue"; import { OpenApiClientButton } from "@scalar/api-client/components"; import { createApiClientModal } from "@scalar/api-client/v2/features/modal"; import { getActiveEnvironment } from "@scalar/api-client/v2/helpers"; import { addScalarClassesToHeadless, ScalarSidebarFooter, ScalarColorModeToggleButton, ScalarColorModeToggleIcon } from "@scalar/components"; import { redirectToProxy } from "@scalar/helpers/url/redirect-to-proxy"; import { createSidebarState, ScalarSidebar } from "@scalar/sidebar"; import { hasObtrusiveScrollbars, getThemeStyles } from "@scalar/themes"; import { apiReferenceConfigurationSchema } from "@scalar/types/api-reference"; import { useBreakpoints } from "@scalar/use-hooks/useBreakpoints"; import { useClipboard } from "@scalar/use-hooks/useClipboard"; import { useColorMode } from "@scalar/use-hooks/useColorMode"; import { ScalarToasts } from "@scalar/use-toasts"; import { createWorkspaceStore } from "@scalar/workspace-store/client"; import { createWorkspaceEventBus } from "@scalar/workspace-store/events"; import diff from "microdiff"; import ClassicHeader from "./ClassicHeader.vue.js"; import _sfc_main$4 from "./Content/Content.vue.js"; /* empty css */ import _sfc_main$1 from "./MobileHeader.vue.js"; import _sfc_main$2 from "../features/multiple-documents/DocumentSelector.vue.js"; import _sfc_main$3 from "../features/Search/components/SearchButton.vue.js"; import _sfc_main$5 from "../features/toolbar/ApiReferenceToolbar.vue.js"; import { getSystemModePreference } from "../helpers/color-mode.js"; import { downloadDocument } from "../helpers/download.js"; import { getIdFromUrl, makeUrlFromId } from "../helpers/id-routing.js"; import { intersectionEnabled, scrollToLazy, blockIntersection } from "../helpers/lazy-bus.js"; import { loadClientFromStorage, loadAuthSchemesFromStorage } from "../helpers/load-from-perssistance.js"; import { mapConfigPlugins } from "../helpers/map-config-plugins.js"; import { mapConfigToWorkspaceStore } from "../helpers/map-config-to-workspace-store.js"; import { normalizeConfigurations } from "../helpers/normalize-configurations.js"; import { useIntersection } from "../hooks/use-intersection.js"; import { persistencePlugin } from "../plugins/persistance-plugin.js"; import { PLUGIN_MANAGER_SYMBOL } from "../plugins/hooks/usePluginManager.js"; import { createPluginManager } from "../plugins/plugin-manager.js"; const _hoisted_1 = { key: 1, class: "flex flex-col p-3 pt-1.5" }; const _hoisted_2 = { key: 1 }; const _hoisted_3 = ["aria-label"]; const _hoisted_4 = { class: "w-64 *:!p-0 empty:hidden" }; const _hoisted_5 = { key: 1, class: "references-footer" }; const _sfc_main = /* @__PURE__ */ defineComponent({ __name: "ApiReference", props: { configuration: {} }, setup(__props, { expose: __expose }) { const props = __props; const { mediaQueries } = useBreakpoints(); const { copyToClipboard } = useClipboard(); const isDevelopment = false; const obtrusiveScrollbars = computed(hasObtrusiveScrollbars); const eventBus = createWorkspaceEventBus({ debug: isDevelopment }); const isSidebarOpen = ref(false); watch( () => mediaQueries?.lg?.value, (newValue, oldValue) => { if (oldValue && !newValue) { isSidebarOpen.value = false; } } ); provideUseId(() => useId()); const configList = computed(() => normalizeConfigurations(props.configuration)); const isMultiDocument = computed(() => Object.keys(configList.value).length > 1); const activeSlug = ref( Object.values(configList.value).find((c) => c.default)?.slug ?? configList.value[Object.keys(configList.value)?.[0] ?? ""]?.slug ?? "" ); if (typeof window !== "undefined") { const url = new URL(window.location.href); const apiParam = url.searchParams.get("api"); if (apiParam && configList.value[apiParam]) { activeSlug.value = apiParam; const idFromUrl = getIdFromUrl( url, configList.value[apiParam].config.pathRouting?.basePath, apiParam ); const newUrl = makeUrlFromId( idFromUrl, configList.value[apiParam].config.pathRouting?.basePath, isMultiDocument.value ); if (newUrl) { newUrl.searchParams.delete("api"); window.history.replaceState({}, "", newUrl.toString()); } } const basePaths = Object.values(configList.value).map( (c) => c.config.pathRouting?.basePath ); const initialId = getIdFromUrl( url, basePaths.find( (p) => p && url.pathname.startsWith(p.startsWith("/") ? p : `/${p}`) ), isMultiDocument.value ? void 0 : activeSlug.value ); const documentSlug = initialId.split("/")[0]; if (documentSlug && configList.value[documentSlug]) { activeSlug.value = documentSlug; } } const documentOptionList = computed( () => Object.values(configList.value).map((c) => ({ label: c.title, id: c.slug })) ); const configurationOverrides = ref({}); const mergedConfig = computed(() => ({ // Provides a default set of values when the lookup fails ...apiReferenceConfigurationSchema.parse({}), // The active configuration based on the slug ...configList.value[activeSlug.value]?.config, // Any overrides from the localhost toolbar ...configurationOverrides.value })); const basePath = computed(() => mergedConfig.value.pathRouting?.basePath); const themeStyle = computed( () => getThemeStyles(mergedConfig.value.theme, { fonts: mergedConfig.value.withDefaultFonts }) ); provide( PLUGIN_MANAGER_SYMBOL, createPluginManager({ plugins: Object.values(configList.value).flatMap( (c) => c.config.plugins ?? [] ) }) ); if (mergedConfig.value.redirect && typeof window !== "undefined") { const newPath = mergedConfig.value.redirect( (mergedConfig.value.pathRouting ? window.location.pathname : "") + window.location.hash ); if (newPath) { window.history.replaceState({}, "", newPath); } } function syncSlugAndUrlWithDocument(slug, elementId, config) { const url = makeUrlFromId( elementId || slug, config.pathRouting?.basePath, isMultiDocument.value ); if (url) { window.history.replaceState({}, "", url.toString()); } activeSlug.value = slug; } const workspaceStore = createWorkspaceStore({ plugins: [ persistencePlugin({ prefix: () => activeSlug.value, persistAuth: () => mergedConfig.value.persistAuth ?? false }) ] }); const { toggleColorMode, isDarkMode } = useColorMode({ initialColorMode: { true: "dark", false: "light", undefined: "system" }[String(mergedConfig.value.darkMode)], overrideColorMode: mergedConfig.value.forceDarkModeState }); const itemsFromWorkspace = computed(() => { return Object.entries(workspaceStore.workspace.documents).map( ([slug, document]) => ({ id: slug, type: "document", description: document.info.description, name: document.info.title ?? slug, title: document.info.title ?? slug, children: document?.["x-scalar-navigation"]?.children ?? [] }) ); }); const sidebarState = createSidebarState(itemsFromWorkspace, { hooks: {} }); const setChildrenOpen = (items) => { items.forEach((item) => { if (item.type === "tag" || item.type === "models") { sidebarState.setExpanded(item.id, true); } if ("children" in item && item.children) { setChildrenOpen(item.children); } }); }; const sidebarItems = computed(() => { const config = mergedConfig.value; if (!config) { return []; } const docItems = sidebarState.items.value.find( (item) => item.id === activeSlug.value )?.children ?? []; if (config.defaultOpenAllTags) { setChildrenOpen(docItems); } if (config.expandAllModelSections) { const models = docItems.find( (item) => item.type === "models" ); if (models) { sidebarState.setExpanded(models.id, true); models.children?.forEach((child) => { sidebarState.setExpanded(child.id, true); }); } } return docItems.filter( (item) => config.hideModels ? item.id !== "models" : true ); }); const infoSectionId = computed( () => sidebarItems.value.find( (item) => item.type === "text" && item.title === "Introduction" )?.id ); const breadcrumb = ref(""); const slotProps = computed(() => ({ breadcrumb: breadcrumb.value })); const setBreadcrumb = (id) => { const item = sidebarState.getEntryById(id); if (!item || item.type === "document") { breadcrumb.value = ""; } else { breadcrumb.value = item.title; } }; const scrollToLazyElement = (id) => { setBreadcrumb(id); sidebarState.setSelected(id); scrollToLazy(id, sidebarState.setExpanded, sidebarState.getEntryById); }; mapConfigToWorkspaceStore({ config: () => mergedConfig.value, store: workspaceStore, isDarkMode }); const environment = computed( () => getActiveEnvironment( workspaceStore, workspaceStore.workspace.activeDocument ?? null ) ); if (typeof window !== "undefined") { window.dataDumpWorkspace = () => workspaceStore; } __expose({ eventBus, workspaceStore }); const changeSelectedDocument = async (slug, elementId) => { const normalized = configList.value[slug]; if (!normalized) { console.warn(`Document ${slug} not found in configList`); return; } const config = { ...normalized.config, ...configurationOverrides.value }; const onDocumentSelectPromise = config.onDocumentSelect?.(); syncSlugAndUrlWithDocument(slug, elementId, config); apiClient.value?.route({ documentSlug: slug, method: "get", path: "/" }); const isFirstLoad = !workspaceStore.workspace.documents[slug]; if (isFirstLoad) { await workspaceStore.addDocument( normalized.source.url ? { name: slug, url: normalized.source.url, fetch: config.fetch } : { name: slug, document: normalized.source.content ?? {} }, config ); } workspaceStore.update("x-scalar-active-document", slug); if (config.persistAuth) { loadAuthSchemesFromStorage(workspaceStore); } void (async () => { await onDocumentSelectPromise; void config.onLoaded?.(slug); })(); if (elementId && elementId !== slug) { scrollToLazyElement(elementId); } else { const firstTag = sidebarItems.value.find((item) => item.type === "tag"); if (firstTag) { sidebarState.setExpanded(firstTag.id, true); } } }; watch( () => Object.values(configList.value), async (newConfigList, oldConfigList) => { const updateSource = async (updated, previous) => { if (!workspaceStore.workspace.documents[updated.slug]) { return; } if (updated.source.url && updated.source.url !== previous?.source.url) { await workspaceStore.addDocument( { name: updated.slug, url: updated.source.url, fetch: updated.config.fetch }, updated.config ); return; } if (!updated.source.content) { return; } if (diff( updated.source.content, previous && "content" in previous.source ? previous.source.content ?? {} : {} ).length) { await workspaceStore.addDocument( { name: updated.slug, document: updated.source.content }, updated.config ); } }; newConfigList.forEach( (newConfig, index) => updateSource(newConfig, oldConfigList[index]) ); const newSlugs = newConfigList.map((c) => c.slug); const oldSlugs = oldConfigList.map((c) => c.slug); if (newSlugs.length !== oldSlugs.length || !newSlugs.every((slug, index) => slug === oldSlugs[index])) { await changeSelectedDocument(newSlugs[0] ?? ""); } }, { deep: true } ); onServerPrefetch(() => changeSelectedDocument(activeSlug.value)); onBeforeMount(async () => { loadClientFromStorage(workspaceStore); await changeSelectedDocument( activeSlug.value, getIdFromUrl( window.location.href, configList.value[activeSlug.value]?.config.pathRouting?.basePath, isMultiDocument.value ? void 0 : activeSlug.value ) ); }); const documentUrl = computed(() => { return configList.value[activeSlug.value]?.source?.url; }); const modal = useTemplateRef("modal"); const apiClient = ref(null); onMounted(() => { if (!modal.value) { return; } apiClient.value = createApiClientModal({ el: modal.value, eventBus, workspaceStore, options: mergedConfig, plugins: mapConfigPlugins(mergedConfig) }); }); onBeforeUnmount(() => { apiClient.value?.app.unmount(); }); eventBus.on( "server:update:selected", ({ url }) => mergedConfig.value.onServerChange?.(url) ); eventBus.on("ui:download:document", async ({ format }) => { if (format === "direct") { const url = configList.value[activeSlug.value]?.source?.url; if (!url) { console.error( "Direct download is not supported for documents without a URL source" ); return; } const result = await fetch( redirectToProxy(mergedConfig.value.proxyUrl, url) ).then((r) => r.text()); downloadDocument(result, activeSlug.value ?? "openapi"); return; } const document = workspaceStore.exportActiveDocument(format); if (!document) { console.error("No document found to download"); return; } downloadDocument(document, activeSlug.value ?? "openapi", format); }); const handleSelectItem = (id, caller) => { const item = sidebarState.getEntryById(id); if ((item?.type === "tag" || item?.type === "models") && sidebarState.isExpanded(id)) { const unblock = blockIntersection(); sidebarState.setExpanded(id, false); unblock(); return; } if (item?.type !== "tag" && item?.type !== "models") { isSidebarOpen.value = false; } scrollToLazyElement(id); const url = makeUrlFromId(id, basePath.value, isMultiDocument.value); if (url) { window.history.pushState({}, "", url); if (caller === "sidebar") { mergedConfig.value.onSidebarClick?.(url.toString()); } } }; eventBus.on("select:nav-item", ({ id }) => handleSelectItem(id)); eventBus.on("scroll-to:nav-item", ({ id }) => handleSelectItem(id)); eventBus.on("intersecting:nav-item", ({ id }) => { if (!intersectionEnabled.value) { return; } sidebarState.setSelected(id); setBreadcrumb(id); const url = makeUrlFromId(id, basePath.value, isMultiDocument.value); if (url && workspaceStore.workspace.activeDocument) { window.history.replaceState({}, "", url.toString()); } }); eventBus.on("toggle:nav-item", ({ id, open }) => { if (open) { mergedConfig.value.onShowMore?.(id); } sidebarState.setExpanded(id, open ?? !sidebarState.isExpanded(id)); }); eventBus.on("copy-url:nav-item", ({ id }) => { const url = makeUrlFromId( id, basePath.value, isMultiDocument.value )?.toString(); return url && copyToClipboard(url); }); onBeforeMount(() => { window.history.scrollRestoration = "manual"; addScalarClassesToHeadless(); window.addEventListener("popstate", () => { const id = getIdFromUrl( window.location.href, mergedConfig.value.pathRouting?.basePath, isMultiDocument.value ? void 0 : activeSlug.value ); if (id) { scrollToLazyElement(id); } }); }); const documentStartRef = useTemplateRef("documentStartRef"); useIntersection(documentStartRef, () => { eventBus.emit("intersecting:nav-item", { id: activeSlug.value }); }); const colorMode = computed(() => { const mode = workspaceStore.workspace["x-scalar-color-mode"]; if (mode === "system") { return getSystemModePreference(); } return mode; }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", null, [ (openBlock(), createBlock(resolveDynamicComponent("style"), null, { default: withCtx(() => [ createTextVNode(toDisplayString(mergedConfig.value.customCss) + " " + toDisplayString(themeStyle.value), 1) ]), _: 1 })), createElementVNode("div", { ref: "documentEl", class: normalizeClass(["scalar-app scalar-api-reference references-layout", [ { "scalar-api-references-standalone-mobile": mergedConfig.value.showSidebar, "scalar-scrollbars-obtrusive": obtrusiveScrollbars.value, "references-editable": mergedConfig.value.isEditable, "references-sidebar": mergedConfig.value.showSidebar, "references-sidebar-mobile-open": isSidebarOpen.value, "references-classic": mergedConfig.value.layout === "classic" }, _ctx.$attrs.class ]]) }, [ mergedConfig.value.layout === "modern" ? (openBlock(), createBlock(_sfc_main$1, { key: 0, breadcrumb: breadcrumb.value, isSidebarOpen: isSidebarOpen.value, showSidebar: mergedConfig.value.showSidebar, onToggleSidebar: _cache[2] || (_cache[2] = () => isSidebarOpen.value = !isSidebarOpen.value) }, { search: withCtx(() => [ !mergedConfig.value.hideSearch ? (openBlock(), createBlock(_sfc_main$3, { key: 0, class: "my-2", document: unref(workspaceStore).workspace.activeDocument, eventBus: unref(eventBus), hideModels: mergedConfig.value.hideModels, searchHotKey: mergedConfig.value.searchHotKey, showSidebar: mergedConfig.value.showSidebar }, null, 8, ["document", "eventBus", "hideModels", "searchHotKey", "showSidebar"])) : createCommentVNode("", true) ]), sidebar: withCtx(({ sidebarClasses }) => [ mergedConfig.value.showSidebar && mergedConfig.value.layout === "modern" ? (openBlock(), createBlock(unref(ScalarSidebar), { key: 0, "aria-label": `Sidebar for ${unref(workspaceStore).workspace.activeDocument?.info?.title}`, class: normalizeClass(["t-doc__sidebar", sidebarClasses]), isExpanded: unref(sidebarState).isExpanded, isSelected: unref(sidebarState).isSelected, items: sidebarItems.value, layout: "reference", options: mergedConfig.value, role: "navigation", onSelectItem: _cache[1] || (_cache[1] = (id) => handleSelectItem(id, "sidebar")) }, { header: withCtx(() => [ documentOptionList.value.length > 1 ? (openBlock(), createBlock(_sfc_main$2, { key: 0, modelValue: activeSlug.value, options: documentOptionList.value, "onUpdate:modelValue": changeSelectedDocument }, null, 8, ["modelValue", "options"])) : createCommentVNode("", true), !mergedConfig.value.hideSearch ? (openBlock(), createElementBlock("div", _hoisted_1, [ createVNode(_sfc_main$3, { document: unref(workspaceStore).workspace.activeDocument, eventBus: unref(eventBus), hideModels: mergedConfig.value.hideModels, searchHotKey: mergedConfig.value.searchHotKey }, null, 8, ["document", "eventBus", "hideModels", "searchHotKey"]) ])) : createCommentVNode("", true), renderSlot(_ctx.$slots, "sidebar-start", normalizeProps(guardReactiveProps(slotProps.value)), void 0, true) ]), footer: withCtx(() => [ renderSlot(_ctx.$slots, "sidebar-end", normalizeProps(guardReactiveProps(slotProps.value)), () => [ createVNode(unref(ScalarSidebarFooter), { class: "darklight-reference" }, { toggle: withCtx(() => [ !mergedConfig.value.hideDarkModeToggle && !mergedConfig.value.forceDarkModeState ? (openBlock(), createBlock(unref(ScalarColorModeToggleButton), { key: 0, modelValue: colorMode.value === "dark", "onUpdate:modelValue": _cache[0] || (_cache[0] = () => unref(toggleColorMode)()) }, null, 8, ["modelValue"])) : (openBlock(), createElementBlock("span", _hoisted_2)) ]), default: withCtx(() => [ !mergedConfig.value.hideClientButton ? (openBlock(), createBlock(unref(OpenApiClientButton), { key: 0, buttonSource: "sidebar", integration: mergedConfig.value._integration, isDevelopment: unref(isDevelopment), url: documentUrl.value }, null, 8, ["integration", "isDevelopment", "url"])) : createCommentVNode("", true) ]), _: 1 }) ], true) ]), _: 3 }, 8, ["aria-label", "class", "isExpanded", "isSelected", "items", "options"])) : createCommentVNode("", true) ]), _: 3 }, 8, ["breadcrumb", "isSidebarOpen", "showSidebar"])) : createCommentVNode("", true), createElementVNode("main", { "aria-label": `Open API Documentation for ${unref(workspaceStore).workspace.activeDocument?.info?.title}`, class: "references-rendered" }, [ createVNode(_sfc_main$4, { document: unref(workspaceStore).workspace.activeDocument, environment: environment.value, eventBus: unref(eventBus), expandedItems: unref(sidebarState).expandedItems.value, headingSlugGenerator: mergedConfig.value.generateHeadingSlug ?? ((heading) => `${activeSlug.value}/description/${heading.slug}`), infoSectionId: infoSectionId.value ?? "description/introduction", items: sidebarItems.value, options: mergedConfig.value, xScalarDefaultClient: unref(workspaceStore).workspace["x-scalar-default-client"] }, createSlots({ start: withCtx(() => [ unref(workspaceStore).workspace.activeDocument && unref(mediaQueries).lg.value ? (openBlock(), createBlock(_sfc_main$5, { key: 0, overrides: configurationOverrides.value, "onUpdate:overrides": _cache[3] || (_cache[3] = ($event) => configurationOverrides.value = $event), configuration: mergedConfig.value, workspace: unref(workspaceStore) }, null, 8, ["overrides", "configuration", "workspace"])) : createCommentVNode("", true), createElementVNode("div", { ref_key: "documentStartRef", ref: documentStartRef }, null, 512), mergedConfig.value.layout === "classic" ? (openBlock(), createBlock(ClassicHeader, { key: 1 }, { "dark-mode-toggle": withCtx(() => [ !mergedConfig.value.hideDarkModeToggle && !mergedConfig.value.forceDarkModeState ? (openBlock(), createBlock(unref(ScalarColorModeToggleIcon), { key: 0, class: "text-c-2 hover:text-c-1", mode: colorMode.value, style: { "transform": "scale(1.4)" }, variant: "icon", onClick: _cache[4] || (_cache[4] = () => unref(toggleColorMode)()) }, null, 8, ["mode"])) : createCommentVNode("", true) ]), default: withCtx(() => [ createElementVNode("div", _hoisted_4, [ documentOptionList.value.length > 1 ? (openBlock(), createBlock(_sfc_main$2, { key: 0, modelValue: activeSlug.value, options: documentOptionList.value, "onUpdate:modelValue": changeSelectedDocument }, null, 8, ["modelValue", "options"])) : createCommentVNode("", true) ]), !mergedConfig.value.hideSearch ? (openBlock(), createBlock(_sfc_main$3, { key: 0, class: "t-doc__sidebar max-w-64", document: unref(workspaceStore).workspace.activeDocument, eventBus: unref(eventBus), hideModels: mergedConfig.value.hideModels, searchHotKey: mergedConfig.value.searchHotKey }, null, 8, ["document", "eventBus", "hideModels", "searchHotKey"])) : createCommentVNode("", true) ]), _: 1 })) : createCommentVNode("", true), renderSlot(_ctx.$slots, "content-start", normalizeProps(guardReactiveProps(slotProps.value)), void 0, true) ]), end: withCtx(() => [ renderSlot(_ctx.$slots, "content-end", normalizeProps(guardReactiveProps(slotProps.value)), void 0, true) ]), _: 2 }, [ mergedConfig.value.isEditable ? { name: "empty-state", fn: withCtx(() => [ renderSlot(_ctx.$slots, "editor-placeholder", normalizeProps(guardReactiveProps(slotProps.value)), void 0, true) ]), key: "0" } : void 0 ]), 1032, ["document", "environment", "eventBus", "expandedItems", "headingSlugGenerator", "infoSectionId", "items", "options", "xScalarDefaultClient"]) ], 8, _hoisted_3), _ctx.$slots.footer ? (openBlock(), createElementBlock("div", _hoisted_5, [ renderSlot(_ctx.$slots, "footer", normalizeProps(guardReactiveProps(slotProps.value)), void 0, true) ])) : createCommentVNode("", true), createElementVNode("div", { ref_key: "modal", ref: modal }, null, 512) ], 2), createVNode(unref(ScalarToasts)) ]); }; } }); export { _sfc_main as default };