@scalar/api-reference
Version:
Generate beautiful API references from OpenAPI documents
230 lines (229 loc) • 8.76 kB
JavaScript
import { getSchemaParamsFromId } from "./id-routing.js";
import { computed, nextTick, onBeforeUnmount, reactive, ref } from "vue";
import { watchDebounced } from "@vueuse/core";
import { nanoid } from "nanoid";
//#region src/helpers/lazy-bus.ts
/**
* List of items that are in the priority queue and will be rendered first (e.g. scroll target).
*/
var priorityQueue = reactive(/* @__PURE__ */ new Set());
/** List of items that are pending to be loaded (in viewport overscan). */
var pendingQueue = reactive(/* @__PURE__ */ new Set());
/** List of items that are already loaded and stay mounted (no eviction). */
var readyQueue = reactive(/* @__PURE__ */ new Set());
/**
* Flag to indicate if the lazy bus is currently running
* Blocks ID changes while running
*/
var isRunning = ref(false);
/** How long tryScroll keeps retrying to find the element (ms). */
var SCROLL_RETRY_MS = 3e3;
/** Tracks when the initial load is complete. */
var firstLazyLoadComplete = ref(false);
/** List of unique identifiers that are blocking intersection */
var intersectionBlockers = reactive(/* @__PURE__ */ new Set());
var onRenderComplete = /* @__PURE__ */ new Set();
/** Cached content heights so placeholders can match when not rendered. */
var lazyPlaceholderHeights = reactive(/* @__PURE__ */ new Map());
var getLazyPlaceholderHeight = (id) => lazyPlaceholderHeights.get(id);
var setLazyPlaceholderHeight = (id, height) => {
if (!Number.isFinite(height) || height <= 0) return;
lazyPlaceholderHeights.set(id, Math.round(height));
};
/** Adds a one time callback to be executed when the lazy bus has finished loading */
var addLazyCompleteCallback = (callback) => {
if (callback) onRenderComplete.add(callback);
};
/**
* Blocks intersection until the returned unblock callback is run.
* Prevents scroll jump while we render new lazy content.
*/
var blockIntersection = () => {
const blockId = nanoid();
intersectionBlockers.add(blockId);
/** Unblock uses a small delay to ensure the scroll is complete before enabling intersection */
return () => setTimeout(() => intersectionBlockers.delete(blockId), 100);
};
/** If there are any pending blocking operations we disable intersection */
var intersectionEnabled = computed(() => intersectionBlockers.size === 0);
/**
* Processes the full queue: priority first, then pending. Blocks intersection while
* rendering so the viewport does not jump. No eviction — items stay in readyQueue.
*/
var runLazyBus = () => {
if (typeof window === "undefined") return;
if (isRunning.value) return;
isRunning.value = true;
/**
* Sets all the pending elements into the ready queue
* After waiting for Vue to update the DOM we execute the callbacks and unblock intersection
*/
const processQueue = async () => {
const priorityIds = [...priorityQueue];
const pendingIds = [...pendingQueue];
if (priorityIds.length === 0 && pendingIds.length === 0) {
onRenderComplete.forEach((fn) => fn());
onRenderComplete.clear();
isRunning.value = false;
firstLazyLoadComplete.value = true;
return;
}
for (const id of priorityIds) {
readyQueue.add(id);
priorityQueue.delete(id);
}
for (const id of pendingIds) {
readyQueue.add(id);
pendingQueue.delete(id);
}
await nextTick();
onRenderComplete.forEach((fn) => fn());
onRenderComplete.clear();
isRunning.value = false;
firstLazyLoadComplete.value = true;
};
if (window.requestIdleCallback) window.requestIdleCallback(processQueue, { timeout: 1500 });
else nextTick(processQueue);
};
/**
* Run the lazy bus when the queue changes and is not currently running
* Debounce so that multiple changes to the queue are batched together
*
* We must run when the priority queue changes because we rely on finish callbacks
* anytime we request potentially lazy elements. If we don't run when the priority queue changes
* we may not have a finish callback even though the element is set to load.
*/
watchDebounced([
() => pendingQueue.size,
() => priorityQueue.size,
() => isRunning.value
], () => {
if ((pendingQueue.size > 0 || priorityQueue.size > 0) && !isRunning.value) runLazyBus();
}, {
debounce: 300,
maxWait: 1500
});
/**
* We only make elements pending if they are not already in the priority or ready queue
*/
var addToPendingQueue = (id) => {
if (id && !readyQueue.has(id) && !priorityQueue.has(id)) pendingQueue.add(id);
};
/**
* Add elements to the priority queue for immediate rendering.
* We allow adding items already in readyQueue so that callbacks are still triggered,
* but processQueue will skip actual re-rendering for items already ready.
*/
var addToPriorityQueue = (id) => {
if (id && !priorityQueue.has(id)) priorityQueue.add(id);
};
/**
* Request an item to be rendered (e.g. when it re-enters the overscan zone).
*/
var requestLazyRender = (id, priority = false) => {
if (!id || readyQueue.has(id)) return;
if (priority) addToPriorityQueue(id);
else addToPendingQueue(id);
if (!isRunning.value) runLazyBus();
};
/**
* Schedules a single run of the lazy bus so that documents with no Lazy components
* (e.g. no operations, tags, or models) still get firstLazyLoadComplete set and the
* full-viewport placeholder can be hidden. Call from content root on mount.
*/
var scheduleInitialLoadComplete = () => {
if (typeof window === "undefined") return;
window.setTimeout(() => runLazyBus(), 400);
};
/** When an element is unmounted we remove it from all queues */
var resetLazyElement = (id) => {
priorityQueue.delete(id);
pendingQueue.delete(id);
readyQueue.delete(id);
lazyPlaceholderHeights.delete(id);
};
/**
* Tracks the lazy loading state of an element.
* Use isReady (or expanded) to decide whether to render the slot or show a placeholder.
* The element is only added to the queue when it enters the viewport overscan (see Lazy.vue).
*/
function useLazyBus(id) {
onBeforeUnmount(() => {
resetLazyElement(id);
});
return { isReady: computed(() => typeof window === "undefined" || priorityQueue.has(id) || readyQueue.has(id)) };
}
/**
* Scroll to a possibly lazy-loaded element. Expands parents and adds target (and
* parents) to the priority queue, then scrolls after Vue has flushed.
*/
var scrollToLazy = (id, setExpanded, getEntryById) => {
const item = getEntryById(id);
const unfreeze = !readyQueue.has(id) || item?.children?.some((child) => !readyQueue.has(child.id)) ? freeze(id) : void 0;
addLazyCompleteCallback(unfreeze);
const unblock = blockIntersection();
const { rawId } = getSchemaParamsFromId(id);
addToPriorityQueue(id);
addToPriorityQueue(rawId);
if (item?.children) item.children.slice(0, 2).forEach((child) => addToPriorityQueue(child.id));
if (item?.parent) {
const parent = getEntryById(item.parent.id);
const elementIdx = parent?.children?.findIndex((child) => child.id === id);
if (elementIdx !== void 0 && elementIdx >= 0) parent?.children?.slice(elementIdx, elementIdx + 2).forEach((child) => addToPriorityQueue(child.id));
}
setExpanded(rawId, true);
/**
* Recursively expand the parents and set them as a loading priority
* This ensures all parents will be immediately loaded and open
*/
const addParents = (currentId) => {
const parent = getEntryById(currentId)?.parent;
if (parent) {
addToPriorityQueue(parent.id);
setExpanded(parent.id, true);
addParents(parent.id);
}
};
/** Must use the rawId as schema params are not in the navigation tree */
addParents(rawId);
nextTick(() => {
tryScroll(id, Date.now() + SCROLL_RETRY_MS, unblock, unfreeze);
});
};
/**
* Tiny wrapper around the scrollIntoView API
* Retries up to the stopTime in case the element is not yet rendered
*
* @param id - The id of the element to scroll to
* @param stopTime - The time to stop retrying in unix milliseconds
*/
var tryScroll = (id, stopTime, onComplete, onFailure) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ block: "start" });
onComplete();
} else if (Date.now() < stopTime) requestAnimationFrame(() => tryScroll(id, stopTime, onComplete, onFailure));
else {
onComplete();
onFailure?.();
}
};
var freeze = (id) => {
let stop = false;
/**
* Runs until the stop flag is set
* Executes the final frame after stop changes to true
*/
const runFrame = (stopAfterFrame) => {
const element = document.getElementById(id);
if (element) element.scrollIntoView({ block: "start" });
if (!stopAfterFrame) requestAnimationFrame(() => runFrame(stop));
};
runFrame(false);
return () => {
stop = true;
};
};
//#endregion
export { addToPriorityQueue, blockIntersection, firstLazyLoadComplete, getLazyPlaceholderHeight, intersectionEnabled, requestLazyRender, scheduleInitialLoadComplete, scrollToLazy, setLazyPlaceholderHeight, useLazyBus };
//# sourceMappingURL=lazy-bus.js.map