/** * Navigation Helpers * * Shared utilities for page navigation in RuntimePresentation and constructor.tsx. * Handles target page resolution, back navigation detection, and transition blocking. */ import type { RuntimePage } from '../types/runtime'; import type { NavigableElement, NavigationTarget, TransitionPhase, } from '../types/presentation'; import { parseJsonObject } from './parseJson'; /** * Context for resolving history-based back navigation */ export interface NavigationContext { currentPageSlug?: string; previousPageId?: string | null; } /** * UI schema structure for type-safe parsing */ interface UiSchemaStructure { elements?: Array>; } /** * Find navigation element on sourcePage that points to targetPageSlug. * Used for history-based back navigation to get the forward transition. * * @param sourcePage - The page to search for navigation elements * @param targetPageSlug - The target page slug to find * @returns The navigation element pointing to target page, or null if not found */ export const findIncomingNavigationElement = ( sourcePage: { ui_schema_json?: string | Record; slug?: string; }, targetPageSlug: string, ): NavigableElement | null => { // Parse ui_schema_json using shared utility const uiSchema = parseJsonObject( sourcePage.ui_schema_json, {}, ); const elements = Array.isArray(uiSchema.elements) ? uiSchema.elements : []; // Find navigation element pointing to target page const found = elements.find( (el) => isNavigationType(String(el.type || '')) && el.targetPageSlug === targetPageSlug, ); if (!found) return null; // Type assertion after validation return found as unknown as NavigableElement; }; /** * Resolve history-based back navigation target. * Finds the previous page from history and looks up the forward transition that was used to arrive. * * @param pages - Available pages * @param currentPageSlug - Current page slug * @param previousPageId - Previous page ID from history * @returns Navigation target or null if previous page not found */ export const resolveHistoryBackTarget = ( pages: RuntimePage[], currentPageSlug: string, previousPageId: string | null, ): NavigationTarget | null => { if (!previousPageId) return null; const previousPage = pages.find((p) => p.id === previousPageId); if (!previousPage) return null; // Look up the forward navigation element that brought user to current page const incomingElement = findIncomingNavigationElement( previousPage, currentPageSlug, ); return { page: previousPage, pageId: previousPage.id, transitionVideoUrl: incomingElement?.transitionVideoUrl, transitionReverseMode: incomingElement?.transitionReverseMode, reverseVideoUrl: incomingElement?.reverseVideoUrl, isBack: true, }; }; /** * Resolve target page from element navigation properties. * Supports both targetPageSlug (new) and targetPageId (legacy). * Also supports history-based back navigation when navBackMode='history'. * * @param element - Element with navigation properties * @param pages - Available pages to search * @param context - Optional context for history-based navigation * @returns The target page or undefined if not found */ export const resolveNavigationTarget = ( element: NavigableElement, pages: RuntimePage[], context?: NavigationContext, ): NavigationTarget | null => { // Handle history-based back navigation if (isBackNavigation(element) && element.navBackMode === 'history') { return resolveHistoryBackTarget( pages, context?.currentPageSlug || '', context?.previousPageId || null, ); } // Standard target_page mode logic const targetPageSlug = element.targetPageSlug; const legacyTargetPageId = element.targetPageId; let targetPage: RuntimePage | undefined; if (targetPageSlug) { targetPage = pages.find((p) => p.slug === targetPageSlug); } else if (legacyTargetPageId) { targetPage = pages.find((p) => p.id === legacyTargetPageId); } if (!targetPage) { return null; } const isBack = isBackNavigation(element); return { page: targetPage, pageId: targetPage.id, transitionVideoUrl: element.transitionVideoUrl, transitionReverseMode: element.transitionReverseMode, reverseVideoUrl: element.reverseVideoUrl, isBack, }; }; /** * Determine if navigation direction is "back". * Elements with navType='back' or type='navigation_prev' navigate backwards. * * @param element - Element to check * @returns true if this is a back navigation */ export const isBackNavigation = (element: NavigableElement): boolean => { return element.navType === 'back' || element.type === 'navigation_prev'; }; /** * Get navigation direction based on element properties. * * @param element - Element with navigation properties * @returns 'back' or 'forward' */ export const getNavigationDirection = ( element: NavigableElement, ): 'back' | 'forward' => { return isBackNavigation(element) ? 'back' : 'forward'; }; /** * Check if transition is actively blocking navigation. * Navigation should be blocked during preparing or playing phases. * * @param transitionPhase - Current transition phase * @param isBuffering - Whether video is buffering * @returns true if navigation should be blocked */ export const isTransitionBlocking = ( transitionPhase: TransitionPhase, isBuffering: boolean, ): boolean => { const activePhases: TransitionPhase[] = ['preparing', 'playing']; return activePhases.includes(transitionPhase) || isBuffering; }; /** * Check if element has a playable transition. * A transition is playable if: * - Forward navigation: has a video URL * - Back navigation: has a video URL AND a reversed video URL * (reversed video is pre-generated by backend) * * @param element - Element with transition properties * @param direction - Navigation direction * @returns true if element has a playable transition */ export const hasPlayableTransition = ( element: { transitionVideoUrl?: string; reverseVideoUrl?: string; }, direction: 'back' | 'forward' = 'forward', ): boolean => { if (!element.transitionVideoUrl) { return false; } // For back navigation, require pre-reversed video (generated by backend) if (direction === 'back' && !element.reverseVideoUrl) { return false; } return true; }; /** * Check if element is a navigation element type. * * @param elementType - Element type to check * @returns true if element is a navigation type */ export const isNavigationType = (elementType: string): boolean => { return ( elementType === 'navigation_next' || elementType === 'navigation_prev' || elementType === 'navigation' ); };