import React, { ForwardedRef, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { SanityDocument } from '@sanity/types'; // @ts-expect-error failed to get TS to accept this import import sanityClient from 'part:@sanity/base/client'; import { PreviewAction, PreviewDocument, PreviewState, usePreviewReducer, } from '../hooks/preview-reducer-hook'; import { useDebouncedEffect, useSetRefs } from '@nrk/nrkno-sanity-react-utils'; import styles from './IFramePreviewBasic.css'; import { PreviewLoading } from './PreviewLoading'; import { BasicPreviewProps } from '../../sanity-types'; import { DisplayTextsContext } from '../DisplayTextsContext'; import { ensureRevision, registerIFramePreviewListener, resolveUrl, UrlResolver, } from './preview-utils'; export interface IFramePreviewBasicOpts { /** * Iframe url. * * Examples: * * `'https://some-page.example/preview'` * * `` (doc) => `https://some-page.example/${doc._id}` `` * * `` (doc) => Promise.resolve(`https://some-page.example/${doc.slug.current}/preview`) `` * */ url: UrlResolver; /** Transform the SanityDocument available to the previewComponent prior to sending it to the iframe. * After the iframe responds with a groq-event, this method will no longer be invoked.*/ mapDocument?: (previewDocument: SanityDocument) => PreviewDocument | Promise; /** When true, scroll in the iframe will be disabled */ disableScroll?: boolean; /** * Minimum preview update time in milliseconds. Used to debounce document updates. * * After updateDelay has passed, the component will execute the current GROQ query every * ms, until up-to-date data is returned, or until maxRevisionRetries is reached. * * Default: 250 */ updateDelay?: number; /** Default: 5 */ maxRevisionRetries?: number; } export interface IFramePreviewBasicProps extends BasicPreviewProps { children?: ReactNode | undefined; options: IFramePreviewBasicOpts; } const DEFAULT_UPDATE_DELAY = 250; const DEFAULT_MAX_REVISION_RETRIES = 5; interface IFramePreviewInternalProps extends IFramePreviewBasicProps { reload: () => void; } export const IFramePreviewBasic = forwardRef(function IFramePreviewBasic( props: IFramePreviewBasicProps, ref: ForwardedRef ) { if (!props.options?.url) { throw new Error('IFramePreview requires props.options.url to be a string or Promise '); } const id = props.document?.displayed?._id; const [key, setKey] = useState(id); const reload = useCallback(() => setKey(`${Math.random()}`), []); useEffect(() => { id !== key && setKey(id); }, [id, key]); return ( {props.children} ); }); const IFramePreviewInternal = forwardRef(function IFramePreview( props: IFramePreviewInternalProps, forwardRef: ForwardedRef ) { const [state, dispatch] = usePreviewReducer(sanityClient); const url = useUrl(props.options.url, props.document.displayed); const ref = useRef(null); useIframeMessageListener(dispatch); useUpdatedPreviewDoc(props.document.displayed, props.options, state, dispatch); useUpdatedIframe(state, dispatch, ref, url); const { iframeTitle } = useContext(DisplayTextsContext); const [anyButtonPressed, mouseListener] = useMouseButtonListener(); const setRefs = useSetRefs(ref, forwardRef); const iframeClasses = useMemo( () => styles.iframe + (!state.iframeReady ? ` ${styles.iframeHidden}` : ''), [state.iframeReady] ); return (