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 { isSafari } from '../lib/browserUtils';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
import { backgroundAudioController } from '../lib/backgroundAudioController';
|
import { backgroundAudioController } from '../lib/backgroundAudioController';
|
||||||
|
import {
|
||||||
|
exitNativeFullscreen,
|
||||||
|
getFullscreenElement,
|
||||||
|
requestDocumentFullscreen,
|
||||||
|
requestParentFullscreenExit,
|
||||||
|
} from '../lib/fullscreen';
|
||||||
import {
|
import {
|
||||||
resolveNavigationTarget,
|
resolveNavigationTarget,
|
||||||
isTransitionBlocking,
|
isTransitionBlocking,
|
||||||
@ -473,24 +479,40 @@ export default function RuntimePresentation({
|
|||||||
|
|
||||||
const toggleFullscreen = useCallback(async () => {
|
const toggleFullscreen = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (!document.fullscreenElement) {
|
const fullscreenElement = getFullscreenElement();
|
||||||
await document.documentElement.requestFullscreen();
|
if (!isFullscreen && !fullscreenElement) {
|
||||||
setIsFullscreen(true);
|
const entered = await requestDocumentFullscreen(
|
||||||
} else {
|
'runtime-global-control',
|
||||||
await document.exitFullscreen();
|
);
|
||||||
setIsFullscreen(false);
|
setIsFullscreen(entered);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fullscreenElement) {
|
||||||
|
await exitNativeFullscreen();
|
||||||
|
} else {
|
||||||
|
requestParentFullscreenExit('runtime-global-control');
|
||||||
|
}
|
||||||
|
setIsFullscreen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const parentExitRequested = requestParentFullscreenExit(
|
||||||
|
'runtime-global-control',
|
||||||
|
);
|
||||||
|
if (parentExitRequested) {
|
||||||
|
setIsFullscreen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
'Fullscreen toggle failed:',
|
'Fullscreen toggle failed:',
|
||||||
err instanceof Error ? err : { error: err },
|
err instanceof Error ? err : { error: err },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [isFullscreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFullscreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
setIsFullscreen(Boolean(document.fullscreenElement));
|
setIsFullscreen(Boolean(getFullscreenElement()));
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
|||||||
@ -19,6 +19,13 @@ import { resolveAssetPlaybackUrl } from '../../lib/assetUrl';
|
|||||||
import { getFontByKey, getFontStyle } from '../../lib/fonts';
|
import { getFontByKey, getFontStyle } from '../../lib/fonts';
|
||||||
import { useGlobalAudioMute } from '../../hooks/useGlobalAudioMute';
|
import { useGlobalAudioMute } from '../../hooks/useGlobalAudioMute';
|
||||||
import { buildChromeFreeEmbedUrl, isValidEmbedUrl } from '../../lib/embedUrl';
|
import { buildChromeFreeEmbedUrl, isValidEmbedUrl } from '../../lib/embedUrl';
|
||||||
|
import {
|
||||||
|
exitNativeFullscreen,
|
||||||
|
getFullscreenElement,
|
||||||
|
isNativeFullscreenElement,
|
||||||
|
requestElementFullscreen,
|
||||||
|
requestEmbeddingFrameFullscreen,
|
||||||
|
} from '../../lib/fullscreen';
|
||||||
|
|
||||||
interface ImageDetailPanelProps {
|
interface ImageDetailPanelProps {
|
||||||
element: CanvasElement;
|
element: CanvasElement;
|
||||||
@ -32,72 +39,6 @@ interface ImageDetailPanelProps {
|
|||||||
onDetailPositionChange?: (xPercent: number, yPercent: number) => void;
|
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> = ({
|
const ImageDetailPanel: React.FC<ImageDetailPanelProps> = ({
|
||||||
element,
|
element,
|
||||||
image,
|
image,
|
||||||
@ -197,26 +138,11 @@ const ImageDetailPanel: React.FC<ImageDetailPanelProps> = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Enter fullscreen
|
// Enter fullscreen
|
||||||
if (panel.requestFullscreen) {
|
await requestElementFullscreen(panel);
|
||||||
await panel.requestFullscreen();
|
|
||||||
} else if (
|
|
||||||
(
|
|
||||||
panel as HTMLDivElement & {
|
|
||||||
webkitRequestFullscreen?: () => Promise<void>;
|
|
||||||
}
|
|
||||||
).webkitRequestFullscreen
|
|
||||||
) {
|
|
||||||
// Safari fallback
|
|
||||||
await (
|
|
||||||
panel as HTMLDivElement & {
|
|
||||||
webkitRequestFullscreen: () => Promise<void>;
|
|
||||||
}
|
|
||||||
).webkitRequestFullscreen();
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Fullscreen can be blocked inside iframes. Try to fullscreen the
|
// Fullscreen can be blocked inside iframes. Try to fullscreen the
|
||||||
// embedding iframe before falling back to filling the iframe viewport.
|
// embedding iframe before falling back to filling the iframe viewport.
|
||||||
await requestEmbeddingFrameFullscreen();
|
await requestEmbeddingFrameFullscreen('image-detail-panel');
|
||||||
} finally {
|
} finally {
|
||||||
setIsFullscreen(true);
|
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