238 lines
6.8 KiB
TypeScript
238 lines
6.8 KiB
TypeScript
/**
|
|
* 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<Record<string, unknown>>;
|
|
}
|
|
|
|
/**
|
|
* 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<string, unknown>;
|
|
slug?: string;
|
|
},
|
|
targetPageSlug: string,
|
|
): NavigableElement | null => {
|
|
// Parse ui_schema_json using shared utility
|
|
const uiSchema = parseJsonObject<UiSchemaStructure>(
|
|
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, playing, or reversing 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', 'reversing'];
|
|
return activePhases.includes(transitionPhase) || isBuffering;
|
|
};
|
|
|
|
/**
|
|
* Check if element has a playable transition.
|
|
* A transition is playable if it has a video URL, and for back navigation,
|
|
* either supports reverse or has a separate reverse video.
|
|
*
|
|
* @param element - Element with transition properties
|
|
* @param direction - Navigation direction
|
|
* @returns true if element has a playable transition
|
|
*/
|
|
export const hasPlayableTransition = (
|
|
element: {
|
|
transitionVideoUrl?: string;
|
|
transitionReverseMode?: string;
|
|
reverseVideoUrl?: string;
|
|
},
|
|
direction: 'back' | 'forward' = 'forward',
|
|
): boolean => {
|
|
if (!element.transitionVideoUrl) {
|
|
return false;
|
|
}
|
|
|
|
// For back navigation with separate_video mode, need reverse video
|
|
if (
|
|
direction === 'back' &&
|
|
element.transitionReverseMode === 'separate_video' &&
|
|
!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'
|
|
);
|
|
};
|