diff --git a/RWSTemplate/partials/scripts.tmpl.partial b/RWSTemplate/partials/scripts.tmpl.partial
index c3a28aa..954bccd 100644
--- a/RWSTemplate/partials/scripts.tmpl.partial
+++ b/RWSTemplate/partials/scripts.tmpl.partial
@@ -3,3 +3,5 @@
+
+
diff --git a/RWSTemplate/styles/custom-scripts.js b/RWSTemplate/styles/custom-scripts.js
new file mode 100644
index 0000000..f7857b5
--- /dev/null
+++ b/RWSTemplate/styles/custom-scripts.js
@@ -0,0 +1,429 @@
+// Handle navigation for #/operations/ and #/schemas/ links when using router="memory"
+document.addEventListener('DOMContentLoaded', function () {
+ const apiElement = document.getElementById('api-docs');
+
+ // Handle initial page load with hash
+ if (window.location.hash && (window.location.hash.startsWith('#/operations/') || window.location.hash.startsWith('#/schemas/'))) {
+ setTimeout(() => {
+ const hash = window.location.hash;
+ const route = hash.substring(1);
+ if (apiElement) {
+ apiElement.setAttribute('basePath', route);
+ }
+ }, 500);
+ }
+});
+
+(function () {
+ let operationMap = {};
+ let operationToTagMap = {}; // Map operationId to tag name (from spec)
+ let specData = null; // Store the loaded spec
+
+ function init() {
+ const elementsApi = document.querySelector('elements-api');
+ if (!elementsApi) {
+ console.log("No found, skipping OperationID result injection.");
+ return;
+ }
+
+ const specUrl = elementsApi.getAttribute('apiDescriptionUrl');
+ if (!specUrl) {
+ console.log("No apiDescriptionUrl found on , skipping.");
+ return;
+ }
+
+ console.log('Fetching spec for OperationID injection...', specUrl);
+ fetch(specUrl)
+ .then(response => response.json())
+ .then(spec => {
+ specData = spec;
+ buildMappingsFromSpec(spec);
+ injectTagIds();
+ startObserver();
+ })
+ .catch(err => console.error('Failed to load spec for operationId injection', err));
+ }
+
+ function buildMappingsFromSpec(spec) {
+ if (spec.paths) {
+ for (const [path, methods] of Object.entries(spec.paths)) {
+ for (const [method, op] of Object.entries(methods)) {
+ if (op.operationId) {
+ // Build operation map for injection (method/path -> operationId)
+ const key = `${method.toUpperCase()} ${path}`;
+ operationMap[key] = op.operationId;
+
+ // Build operationId to tag mapping from spec
+ if (op.tags && op.tags.length > 0) {
+ operationToTagMap[op.operationId] = op.tags[0]; // Use first tag
+ console.log(`Mapped ${op.operationId} to tag: ${op.tags[0]}`);
+ }
+ }
+ }
+ }
+ }
+ console.log('Spec loaded. Operation map size:', Object.keys(operationMap).length);
+ console.log('Operation to tag map size:', Object.keys(operationToTagMap).length);
+ }
+
+ function injectTagIds() {
+ // Find all tag name elements first
+ const tagNameElements = document.querySelectorAll('.sl-text-lg.sl-font-medium');
+
+ console.log(`Found ${tagNameElements.length} tag name elements to inject IDs`);
+
+ if (tagNameElements.length === 0) {
+ console.log('No tag name elements found yet, retrying in 1 second...');
+ setTimeout(injectTagIds, 1000);
+ return;
+ }
+
+ tagNameElements.forEach(tagNameEl => {
+ const tagName = tagNameEl.textContent.trim();
+ const parentElement = tagNameEl.parentElement;
+
+ if (parentElement && !parentElement.getAttribute('data-oas-tag-id')) {
+ parentElement.setAttribute('data-oas-tag-id', tagName);
+ console.log(`Injected tag ID: ${tagName} on parent element:`, parentElement);
+ }
+ });
+ }
+
+ function startObserver() {
+ const observer = new MutationObserver((mutations) => {
+ injectOperationIds();
+ injectTagIds(); // Also retry tag ID injection when DOM changes
+ });
+
+ // Observer the body or the container if possible
+ const target = document.querySelector('elements-api') || document.body;
+ observer.observe(target, { childList: true, subtree: true });
+
+ // Also run immediately
+ injectOperationIds();
+ }
+
+ function injectOperationIds() {
+ // Target the operation method/path container.
+ const containers = document.querySelectorAll('.sl-flex.sl-items-center');
+
+ containers.forEach(container => {
+ if (container.dataset.opIdInjected) return;
+
+ const methodEl = container.querySelector('.sl-uppercase');
+ if (!methodEl) return;
+
+ // The path element is usually a div sibling.
+ let pathEl = null;
+ for (const child of container.children) {
+ if (child !== methodEl && child.textContent.trim().startsWith('/')) {
+ pathEl = child;
+ break;
+ }
+ }
+
+ if (methodEl && pathEl) {
+ const method = methodEl.textContent.trim().toUpperCase();
+ const path = pathEl.textContent.trim();
+ const key = `${method} ${path}`;
+
+ const operationId = operationMap[key];
+
+ if (operationId) {
+ // 1. Create a wrapper
+ const wrapper = document.createElement('span');
+ wrapper.style.display = 'inline-flex';
+ wrapper.style.alignItems = 'center';
+
+ // 2. Operation ID Badge
+ const idSpan = document.createElement('span');
+ idSpan.textContent = ` [${operationId}]`;
+ idSpan.style.fontSize = '12px';
+ idSpan.style.color = '#555';
+ idSpan.style.marginLeft = '12px';
+ idSpan.style.fontFamily = 'monospace';
+ idSpan.style.backgroundColor = 'rgba(0,0,0,0.05)';
+ idSpan.style.padding = '2px 6px';
+ idSpan.style.borderRadius = '4px';
+ idSpan.style.border = '1px solid #ddd';
+ idSpan.className = 'inserted-operation-id';
+ idSpan.title = 'Operation ID';
+
+ // 3. Anchor Link
+ const anchorId = `/operations/${operationId}`;
+ const anchorLink = document.createElement('a');
+ anchorLink.href = `#${anchorId}`;
+ anchorLink.textContent = '🔗';
+ anchorLink.style.marginLeft = '8px';
+ anchorLink.style.textDecoration = 'none';
+ anchorLink.style.fontSize = '14px';
+ anchorLink.style.cursor = 'pointer';
+ anchorLink.title = `Permalink to ${operationId}`;
+
+ // Add click handler to always trigger navigation
+ anchorLink.addEventListener('click', (e) => {
+ e.preventDefault();
+ console.log(`Direct click on operation link: ${operationId}`);
+ navigateToOperation(operationId);
+ });
+
+ wrapper.appendChild(idSpan);
+ wrapper.appendChild(anchorLink);
+ container.appendChild(wrapper);
+
+ // 4. Set the ID on the container for scrolling
+ if (!container.id) {
+ container.id = anchorId;
+ } else {
+ // Append invisible anchor target
+ const anchorTarget = document.createElement('a');
+ anchorTarget.id = anchorId;
+ anchorTarget.style.position = 'absolute';
+ anchorTarget.style.top = '-100px'; // Offset for bad headers
+ anchorTarget.style.visibility = 'hidden';
+ container.style.position = 'relative'; // Ensure absolute positioning is relative to container
+ container.appendChild(anchorTarget);
+ }
+
+ container.dataset.opIdInjected = 'true';
+
+ // 5. Check if we need to scroll to this hash (if page loaded with this hash)
+ if (window.location.hash === `#${anchorId}`) {
+ setTimeout(() => {
+ navigateToOperation(operationId);
+ }, 1000); // Give more time for all content to load
+ }
+ }
+ }
+ });
+ }
+
+ // Get tag name from tag section header
+ function getTagName(tagSection) {
+ const tagNameEl = tagSection.querySelector('.sl-text-lg.sl-font-medium');
+ return tagNameEl ? tagNameEl.textContent : 'Unknown';
+ }
+
+ // Handle hash changes for navigation
+ function handleHashChange() {
+ const hash = window.location.hash;
+ if (hash.startsWith('#/operations/')) {
+ const operationId = hash.replace('#/operations/', '');
+ navigateToOperation(operationId);
+ }
+ }
+
+ // Navigate to a specific operation by operationId
+ function navigateToOperation(operationId) {
+ console.log(`Navigating to operation: ${operationId}`);
+
+ // First, try to find the operation by the injected anchor
+ const anchorId = `/operations/${operationId}`;
+ let targetElement = document.getElementById(anchorId);
+
+ if (targetElement && isElementVisible(targetElement)) {
+ // Found it and it's visible, scroll to it
+ console.log(`Operation ${operationId} found and visible, scrolling directly`);
+ scrollToAndHighlight(targetElement);
+ return;
+ }
+
+ // Use the spec-based mapping to find the correct tag section
+ const tagName = operationToTagMap[operationId];
+ if (tagName) {
+ console.log(`Operation ${operationId} belongs to tag: ${tagName}`);
+
+ // Find the tag section by the injected data-oas-tag-id
+ const tagSection = document.querySelector(`[data-oas-tag-id="${tagName}"]`);
+ if (tagSection) {
+ console.log(`Found tag section for: ${tagName}`);
+
+ if (!isTagSectionExpanded(tagSection)) {
+ console.log(`Jumping to and expanding tag section: ${tagName}`);
+
+ // Jump directly to the tag section
+ tagSection.scrollIntoView({ behavior: 'auto', block: 'start' });
+
+ // Expand the tag section
+ tagSection.click();
+
+ // Immediately find and highlight the operation (no delay)
+ console.log(`Tag section expanded, immediately navigating to operation: ${operationId}`);
+ findAndScrollToOperation(operationId);
+ } else {
+ console.log(`Tag section ${tagName} already expanded, finding operation`);
+ // Tag section already expanded, find the operation immediately
+ findAndScrollToOperation(operationId);
+ }
+ } else {
+ console.warn('Tag section not found for tag: %s. Available tag sections:', tagName,
+ Array.from(document.querySelectorAll('[data-oas-tag-id]')).map(el => el.getAttribute('data-oas-tag-id')));
+ }
+ } else {
+ console.warn('No tag mapping found for operation: %s. Available operations:', operationId, Object.keys(operationToTagMap));
+ console.log('No fallback - operation not found in mapping');
+ }
+ }
+
+ // Check if element is visible in the viewport
+ function isElementVisible(element) {
+ const rect = element.getBoundingClientRect();
+ return rect.height > 0 && rect.width > 0;
+ }
+
+ // Find and scroll to operation after tag section is expanded
+ function findAndScrollToOperation(operationId) {
+ const anchorId = `/operations/${operationId}`;
+ let targetElement = document.getElementById(anchorId);
+
+ if (!targetElement) {
+ console.warn(`Operation element not found with ID: ${anchorId}`);
+ return;
+ }
+
+ console.log(`Found operation ${operationId}, scrolling to center and highlighting`);
+ scrollToAndHighlight(targetElement);
+ }
+
+ // Check if a tag section is expanded
+ function isTagSectionExpanded(tagSection) {
+ const chevron = tagSection.querySelector('svg[data-icon="chevron-down"], svg[data-icon="chevron-right"]');
+ return chevron && chevron.getAttribute('data-icon') === 'chevron-down';
+ }
+
+ // Scroll to element and highlight it
+ function scrollToAndHighlight(element) {
+ console.log('Scrolling to and highlighting operation');
+
+ // Use requestAnimationFrame and retry logic for reliable scrolling
+ const performScroll = (retries = 3) => {
+ requestAnimationFrame(() => {
+ // Check if element is properly positioned
+ const rect = element.getBoundingClientRect();
+ if (rect.height === 0 && retries > 0) {
+ console.log('Element not yet positioned, retrying scroll...');
+ setTimeout(() => performScroll(retries - 1), 300);
+ return;
+ }
+
+ // Stop any existing smooth scrolling
+ window.scrollTo({ top: window.pageYOffset, behavior: 'auto' });
+
+ // Small delay to let any competing scrolls settle
+ setTimeout(() => {
+ element.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'nearest'
+ });
+
+ // Highlight after scrolling starts
+ setTimeout(() => {
+ element.style.transition = 'background-color 1s';
+ const originalBg = element.style.backgroundColor;
+ element.style.backgroundColor = '#fff3cd'; // Light yellow highlight
+ setTimeout(() => {
+ element.style.backgroundColor = originalBg;
+ }, 2000);
+ }, 100);
+ }, 50);
+ });
+ };
+
+ performScroll();
+ }
+
+ // Find the operation container that might need expanding
+ function findOperationContainer(element) {
+ // Look for parent elements that might be collapsible operation containers
+ let parent = element.parentElement;
+
+ while (parent && parent !== document.body) {
+ // Look for HttpOperation class or other indicators of operation containers
+ if (parent.classList.contains('HttpOperation') ||
+ parent.querySelector('.HttpOperation') ||
+ parent.querySelector('[data-testid*="operation"]')) {
+ return parent;
+ }
+ parent = parent.parentElement;
+ }
+
+ return null;
+ }
+
+ // Expand operation container if collapsed (now simplified since we handle this in findAndScrollToOperation)
+ function expandOperationContainer(container) {
+ // This function is kept for compatibility but the main expansion logic
+ // is now handled directly in findAndScrollToOperation using the native
+ // Stoplight Elements structure
+ console.log('expandOperationContainer called - expansion handled elsewhere');
+ }
+
+ // Handle hash changes for navigation
+ function handleHashChange() {
+ const hash = window.location.hash;
+ if (hash.startsWith('#/operations/')) {
+ const operationId = hash.replace('#/operations/', '');
+ navigateToOperation(operationId);
+ }
+ }
+
+ window.addEventListener('hashchange', handleHashChange);
+
+ // Expose navigation function globally for testing
+ window.navigateToOperation = navigateToOperation;
+
+ // Expose injection function for debugging
+ window.injectOperationIds = injectOperationIds;
+ window.injectTagIds = injectTagIds;
+
+ // Expose debug functions
+ window.showOperationTagMap = function () {
+ console.log('Operation → Tag Section mapping:');
+ console.log('Total operations mapped:', Object.keys(operationToTagMap).length);
+ if (Object.keys(operationToTagMap).length === 0) {
+ console.log('No operations have been mapped yet. This could mean:');
+ console.log('1. The page hasn\'t fully loaded');
+ console.log('2. The spec hasn\'t been loaded yet');
+ console.log('3. injectOperationIds() hasn\'t run yet');
+ console.log('Try running: injectOperationIds() to manually trigger it');
+ } else {
+ Object.keys(operationToTagMap).forEach(opId => {
+ const tagName = operationToTagMap[opId];
+ console.log(` ${opId} → ${tagName}`);
+ });
+ }
+ };
+
+ window.findOperationInTag = function (tagName) {
+ const operations = [];
+ Object.keys(operationToTagMap).forEach(opId => {
+ const opTagName = operationToTagMap[opId];
+ if (opTagName === tagName) {
+ operations.push(opId);
+ }
+ });
+ console.log(`Operations in "${tagName}" tag:`, operations);
+ return operations;
+ };
+
+ // Handle initial hash on page load
+ function handleInitialHash() {
+ if (window.location.hash && window.location.hash.startsWith('#/operations/')) {
+ const operationId = window.location.hash.replace('#/operations/', '');
+ console.log(`Handling initial hash for operation: ${operationId}`);
+ setTimeout(() => {
+ navigateToOperation(operationId);
+ }, 1500); // Give Stoplight Elements time to fully render
+ }
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+ // Also handle initial hash after initialization
+ setTimeout(handleInitialHash, 2000);
+})();
diff --git a/docfx.json b/docfx.json
index 1a7a76e..f6a94a3 100644
--- a/docfx.json
+++ b/docfx.json
@@ -35,7 +35,8 @@
"files": [
"**/images/**",
"**/*.json",
- "**/*.html"
+ "**/*.html",
+ "**/*.js"
]
}
],