improved full screen functionality
This commit is contained in:
parent
10bb6da35f
commit
f7219ece35
@ -51,6 +51,12 @@ import { downloadManager } from '../lib/offline/DownloadManager';
|
||||
import { isSafari } from '../lib/browserUtils';
|
||||
import { logger } from '../lib/logger';
|
||||
import { backgroundAudioController } from '../lib/backgroundAudioController';
|
||||
import {
|
||||
exitNativeFullscreen,
|
||||
getFullscreenElement,
|
||||
requestDocumentFullscreen,
|
||||
requestParentFullscreenExit,
|
||||
} from '../lib/fullscreen';
|
||||
import {
|
||||
resolveNavigationTarget,
|
||||
isTransitionBlocking,
|
||||
@ -473,24 +479,40 @@ export default function RuntimePresentation({
|
||||
|
||||
const toggleFullscreen = useCallback(async () => {
|
||||
try {
|
||||
if (!document.fullscreenElement) {
|
||||
await document.documentElement.requestFullscreen();
|
||||
setIsFullscreen(true);
|
||||
} else {
|
||||
await document.exitFullscreen();
|
||||
setIsFullscreen(false);
|
||||
const fullscreenElement = getFullscreenElement();
|
||||
if (!isFullscreen && !fullscreenElement) {
|
||||
const entered = await requestDocumentFullscreen(
|
||||
'runtime-global-control',
|
||||
);
|
||||
setIsFullscreen(entered);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fullscreenElement) {
|
||||
await exitNativeFullscreen();
|
||||
} else {
|
||||
requestParentFullscreenExit('runtime-global-control');
|
||||
}
|
||||
setIsFullscreen(false);
|
||||
} catch (err) {
|
||||
const parentExitRequested = requestParentFullscreenExit(
|
||||
'runtime-global-control',
|
||||
);
|
||||
if (parentExitRequested) {
|
||||
setIsFullscreen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(
|
||||
'Fullscreen toggle failed:',
|
||||
err instanceof Error ? err : { error: err },
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
}, [isFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(Boolean(document.fullscreenElement));
|
||||
setIsFullscreen(Boolean(getFullscreenElement()));
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
|
||||
@ -19,6 +19,13 @@ import { resolveAssetPlaybackUrl } from '../../lib/assetUrl';
|
||||
import { getFontByKey, getFontStyle } from '../../lib/fonts';
|
||||
import { useGlobalAudioMute } from '../../hooks/useGlobalAudioMute';
|
||||
import { buildChromeFreeEmbedUrl, isValidEmbedUrl } from '../../lib/embedUrl';
|
||||
import {
|
||||
exitNativeFullscreen,
|
||||
getFullscreenElement,
|
||||
isNativeFullscreenElement,
|
||||
requestElementFullscreen,
|
||||
requestEmbeddingFrameFullscreen,
|
||||
} from '../../lib/fullscreen';
|
||||
|
||||
interface ImageDetailPanelProps {
|
||||
element: CanvasElement;
|
||||
@ -32,72 +39,6 @@ interface ImageDetailPanelProps {
|
||||
onDetailPositionChange?: (xPercent: number, yPercent: number) => void;
|
||||
}
|
||||
|
||||
const getFullscreenElement = (): Element | null => {
|
||||
return (
|
||||
document.fullscreenElement ??
|
||||
(
|
||||
document as Document & {
|
||||
webkitFullscreenElement?: Element | null;
|
||||
}
|
||||
).webkitFullscreenElement ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const isNativeFullscreenElement = (element: Element | null): boolean =>
|
||||
!!element && getFullscreenElement() === element;
|
||||
|
||||
const exitNativeFullscreen = async (): Promise<void> => {
|
||||
if (document.exitFullscreen) {
|
||||
await document.exitFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
const webkitExitFullscreen = (
|
||||
document as Document & {
|
||||
webkitExitFullscreen?: () => Promise<void>;
|
||||
}
|
||||
).webkitExitFullscreen;
|
||||
|
||||
if (webkitExitFullscreen) {
|
||||
await webkitExitFullscreen.call(document);
|
||||
}
|
||||
};
|
||||
|
||||
const requestEmbeddingFrameFullscreen = async (): Promise<boolean> => {
|
||||
if (window.parent === window) return false;
|
||||
|
||||
try {
|
||||
const frame = window.frameElement as
|
||||
| (HTMLElement & {
|
||||
webkitRequestFullscreen?: () => Promise<void>;
|
||||
})
|
||||
| null;
|
||||
|
||||
if (frame?.requestFullscreen) {
|
||||
await frame.requestFullscreen();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (frame?.webkitRequestFullscreen) {
|
||||
await frame.webkitRequestFullscreen();
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin parents are not directly accessible from the iframe.
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'tour-builder:request-fullscreen',
|
||||
source: 'image-detail-panel',
|
||||
},
|
||||
'*',
|
||||
);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const ImageDetailPanel: React.FC<ImageDetailPanelProps> = ({
|
||||
element,
|
||||
image,
|
||||
@ -197,26 +138,11 @@ const ImageDetailPanel: React.FC<ImageDetailPanelProps> = ({
|
||||
|
||||
try {
|
||||
// Enter fullscreen
|
||||
if (panel.requestFullscreen) {
|
||||
await panel.requestFullscreen();
|
||||
} else if (
|
||||
(
|
||||
panel as HTMLDivElement & {
|
||||
webkitRequestFullscreen?: () => Promise<void>;
|
||||
}
|
||||
).webkitRequestFullscreen
|
||||
) {
|
||||
// Safari fallback
|
||||
await (
|
||||
panel as HTMLDivElement & {
|
||||
webkitRequestFullscreen: () => Promise<void>;
|
||||
}
|
||||
).webkitRequestFullscreen();
|
||||
}
|
||||
await requestElementFullscreen(panel);
|
||||
} catch {
|
||||
// Fullscreen can be blocked inside iframes. Try to fullscreen the
|
||||
// embedding iframe before falling back to filling the iframe viewport.
|
||||
await requestEmbeddingFrameFullscreen();
|
||||
await requestEmbeddingFrameFullscreen('image-detail-panel');
|
||||
} finally {
|
||||
setIsFullscreen(true);
|
||||
}
|
||||
|
||||
111
frontend/src/lib/fullscreen.ts
Normal file
111
frontend/src/lib/fullscreen.ts
Normal file
@ -0,0 +1,111 @@
|
||||
export const getFullscreenElement = (): Element | null => {
|
||||
return (
|
||||
document.fullscreenElement ??
|
||||
(
|
||||
document as Document & {
|
||||
webkitFullscreenElement?: Element | null;
|
||||
}
|
||||
).webkitFullscreenElement ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
export const isNativeFullscreenElement = (element: Element | null): boolean =>
|
||||
!!element && getFullscreenElement() === element;
|
||||
|
||||
export const exitNativeFullscreen = async (): Promise<void> => {
|
||||
if (document.exitFullscreen) {
|
||||
await document.exitFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
const webkitExitFullscreen = (
|
||||
document as Document & {
|
||||
webkitExitFullscreen?: () => Promise<void>;
|
||||
}
|
||||
).webkitExitFullscreen;
|
||||
|
||||
if (webkitExitFullscreen) {
|
||||
await webkitExitFullscreen.call(document);
|
||||
}
|
||||
};
|
||||
|
||||
const isEmbeddedWindow = (): boolean => window.parent !== window;
|
||||
|
||||
export const requestElementFullscreen = async (
|
||||
element: HTMLElement,
|
||||
): Promise<void> => {
|
||||
if (element.requestFullscreen) {
|
||||
await element.requestFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
const webkitRequestFullscreen = (
|
||||
element as HTMLElement & {
|
||||
webkitRequestFullscreen?: () => Promise<void>;
|
||||
}
|
||||
).webkitRequestFullscreen;
|
||||
|
||||
if (webkitRequestFullscreen) {
|
||||
await webkitRequestFullscreen.call(element);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Fullscreen API is not available');
|
||||
};
|
||||
|
||||
export const requestEmbeddingFrameFullscreen = async (
|
||||
source = 'runtime',
|
||||
): Promise<boolean> => {
|
||||
if (!isEmbeddedWindow()) return false;
|
||||
|
||||
try {
|
||||
const frame = window.frameElement as
|
||||
| (HTMLElement & {
|
||||
webkitRequestFullscreen?: () => Promise<void>;
|
||||
})
|
||||
| null;
|
||||
|
||||
if (frame) {
|
||||
await requestElementFullscreen(frame);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin parents are not directly accessible from the iframe.
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'tour-builder:request-fullscreen',
|
||||
source,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const requestDocumentFullscreen = async (
|
||||
source = 'runtime',
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await requestElementFullscreen(document.documentElement);
|
||||
return true;
|
||||
} catch {
|
||||
return requestEmbeddingFrameFullscreen(source);
|
||||
}
|
||||
};
|
||||
|
||||
export const requestParentFullscreenExit = (source = 'runtime'): boolean => {
|
||||
if (!isEmbeddedWindow()) return false;
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'tour-builder:exit-fullscreen',
|
||||
source,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user