Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
225 lines
7.2 KiB
Plaintext
225 lines
7.2 KiB
Plaintext
'use client';
|
|
|
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
import { DndContext } from '@dnd-kit/core';
|
|
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
import { usePopupWindow } from '../../hooks/usePopupWindow.js';
|
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js';
|
|
import { usePreferences } from '../../providers/Preferences/index.js';
|
|
import { formatAbsoluteURL } from '../../utilities/formatAbsoluteURL.js';
|
|
import { customCollisionDetection } from './collisionDetection.js';
|
|
import { LivePreviewContext } from './context.js';
|
|
import { sizeReducer } from './sizeReducer.js';
|
|
export const LivePreviewProvider = ({
|
|
breakpoints: incomingBreakpoints,
|
|
children,
|
|
isLivePreviewEnabled,
|
|
isLivePreviewing: incomingIsLivePreviewing,
|
|
isPreviewEnabled,
|
|
previewURL: previewURLFromProps,
|
|
typeofLivePreviewURL,
|
|
url: urlFromProps
|
|
}) => {
|
|
const [previewWindowType, setPreviewWindowType] = useState('iframe');
|
|
const [isLivePreviewing, setIsLivePreviewing] = useState(incomingIsLivePreviewing);
|
|
const breakpoints = useMemo(() => [...(incomingBreakpoints || []), {
|
|
name: 'responsive',
|
|
height: '100%',
|
|
label: 'Responsive',
|
|
width: '100%'
|
|
}], [incomingBreakpoints]);
|
|
const [url, setURL] = useState('');
|
|
const [previewURL, setPreviewURL] = useState(previewURLFromProps);
|
|
const {
|
|
isPopupOpen,
|
|
openPopupWindow,
|
|
popupRef
|
|
} = usePopupWindow({
|
|
eventType: 'payload-live-preview',
|
|
url
|
|
});
|
|
const [appIsReady, setAppIsReady] = useState(false);
|
|
const [listeningForMessages, setListeningForMessages] = useState(false);
|
|
const {
|
|
collectionSlug,
|
|
globalSlug
|
|
} = useDocumentInfo();
|
|
const isFirstRender = useRef(true);
|
|
const {
|
|
setPreference
|
|
} = usePreferences();
|
|
const iframeRef = React.useRef(null);
|
|
const [loadedURL, setLoadedURL] = useState();
|
|
const [zoom, setZoom] = useState(1);
|
|
const [position, setPosition] = useState({
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
const [size, setSize] = React.useReducer(sizeReducer, {
|
|
height: 0,
|
|
width: 0
|
|
});
|
|
const [measuredDeviceSize, setMeasuredDeviceSize] = useState({
|
|
height: 0,
|
|
width: 0
|
|
});
|
|
const [breakpoint, setBreakpoint] = React.useState('responsive');
|
|
/**
|
|
* A "middleware" callback fn that does some additional work before `setURL`.
|
|
* This is what we provide through context, bc it:
|
|
* - ensures the URL is absolute
|
|
* - resets `appIsReady` to `false` while the new URL is loading
|
|
*/
|
|
const setLivePreviewURL = useCallback(_incomingURL => {
|
|
let incomingURL;
|
|
if (typeof _incomingURL === 'string') {
|
|
incomingURL = formatAbsoluteURL(_incomingURL);
|
|
}
|
|
if (!incomingURL) {
|
|
setIsLivePreviewing(false);
|
|
}
|
|
if (incomingURL !== url) {
|
|
setAppIsReady(false);
|
|
setURL(incomingURL);
|
|
}
|
|
}, [url]);
|
|
/**
|
|
* `url` needs to be relative to the window, which cannot be done on initial render.
|
|
*/
|
|
useEffect(() => {
|
|
if (typeof urlFromProps === 'string') {
|
|
setURL(formatAbsoluteURL(urlFromProps));
|
|
}
|
|
}, [urlFromProps]);
|
|
// The toolbar needs to freely drag and drop around the page
|
|
const handleDragEnd = ev => {
|
|
// only update position if the toolbar is completely within the preview area
|
|
// otherwise reset it back to the previous position
|
|
// TODO: reset to the nearest edge of the preview area
|
|
if (ev.over && ev.over.id === 'live-preview-area') {
|
|
const newPos = {
|
|
x: position.x + ev.delta.x,
|
|
y: position.y + ev.delta.y
|
|
};
|
|
setPosition(newPos);
|
|
} else {
|
|
// reset
|
|
}
|
|
};
|
|
const setWidth = useCallback(width => {
|
|
setSize({
|
|
type: 'width',
|
|
value: width
|
|
});
|
|
}, [setSize]);
|
|
const setHeight = useCallback(height => {
|
|
setSize({
|
|
type: 'height',
|
|
value: height
|
|
});
|
|
}, [setSize]);
|
|
// explicitly set new width and height when as new breakpoints are selected
|
|
// exclude `custom` breakpoint as it is handled by the `setWidth` and `setHeight` directly
|
|
useEffect(() => {
|
|
const foundBreakpoint = breakpoints?.find(bp => bp.name === breakpoint);
|
|
if (foundBreakpoint && breakpoint !== 'responsive' && breakpoint !== 'custom' && typeof foundBreakpoint?.width === 'number' && typeof foundBreakpoint?.height === 'number') {
|
|
setSize({
|
|
type: 'reset',
|
|
value: {
|
|
height: foundBreakpoint.height,
|
|
width: foundBreakpoint.width
|
|
}
|
|
});
|
|
}
|
|
}, [breakpoint, breakpoints]);
|
|
/**
|
|
* Receive the `ready` message from the popup window
|
|
* This indicates that the app is ready to receive `window.postMessage` events
|
|
* This is also the only cross-origin way of detecting when a popup window has loaded
|
|
* Unlike iframe elements which have an `onLoad` handler, there is no way to access `window.open` on popups
|
|
*/
|
|
useEffect(() => {
|
|
const handleMessage = event => {
|
|
if (url?.startsWith(event.origin) && event.data && typeof event.data === 'object' && event.data.type === 'payload-live-preview') {
|
|
if (event.data.ready) {
|
|
setAppIsReady(true);
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener('message', handleMessage);
|
|
setListeningForMessages(true);
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage);
|
|
};
|
|
}, [url, listeningForMessages]);
|
|
const handleWindowChange = useCallback(type => {
|
|
setAppIsReady(false);
|
|
setPreviewWindowType(type);
|
|
if (type === 'popup') {
|
|
openPopupWindow();
|
|
}
|
|
}, [openPopupWindow]);
|
|
// when the user closes the popup window, switch back to the iframe
|
|
// the `usePopupWindow` reports the `isPopupOpen` state for us to use here
|
|
useEffect(() => {
|
|
const newPreviewWindowType = isPopupOpen ? 'popup' : 'iframe';
|
|
if (newPreviewWindowType !== previewWindowType) {
|
|
handleWindowChange('iframe');
|
|
}
|
|
}, [previewWindowType, isPopupOpen, handleWindowChange]);
|
|
useEffect(() => {
|
|
if (isFirstRender.current) {
|
|
isFirstRender.current = false;
|
|
return;
|
|
}
|
|
void setPreference(collectionSlug ? `collection-${collectionSlug}` : `global-${globalSlug}`, {
|
|
editViewType: isLivePreviewing ? 'live-preview' : 'default'
|
|
}, true);
|
|
}, [isLivePreviewing, setPreference, collectionSlug, globalSlug]);
|
|
const dndContextID = useId();
|
|
return /*#__PURE__*/_jsx(LivePreviewContext, {
|
|
value: {
|
|
appIsReady,
|
|
breakpoint,
|
|
breakpoints,
|
|
iframeRef,
|
|
isLivePreviewEnabled,
|
|
isLivePreviewing,
|
|
isPopupOpen,
|
|
isPreviewEnabled,
|
|
listeningForMessages,
|
|
loadedURL,
|
|
measuredDeviceSize,
|
|
openPopupWindow,
|
|
popupRef,
|
|
previewURL,
|
|
previewWindowType,
|
|
setAppIsReady,
|
|
setBreakpoint,
|
|
setHeight,
|
|
setIsLivePreviewing,
|
|
setLoadedURL,
|
|
setMeasuredDeviceSize,
|
|
setPreviewURL,
|
|
setPreviewWindowType: handleWindowChange,
|
|
setSize,
|
|
setToolbarPosition: setPosition,
|
|
setURL: setLivePreviewURL,
|
|
setWidth,
|
|
setZoom,
|
|
size,
|
|
toolbarPosition: position,
|
|
typeofLivePreviewURL,
|
|
url,
|
|
zoom
|
|
},
|
|
children: /*#__PURE__*/_jsx(DndContext, {
|
|
collisionDetection: customCollisionDetection,
|
|
// Provide stable ID to fix hydration issues: https://github.com/clauderic/dnd-kit/issues/926
|
|
id: dndContextID,
|
|
onDragEnd: handleDragEnd,
|
|
children: children
|
|
})
|
|
});
|
|
};
|
|
//# sourceMappingURL=index.js.map |