chore: optimizes live preview (#3713)

This commit is contained in:
Jacob Fletcher
2023-10-17 12:42:31 -04:00
committed by GitHub
parent dd0ac066ce
commit cdbfc9132a
11 changed files with 325 additions and 272 deletions

View File

@@ -3,21 +3,31 @@ import type { Dispatch } from 'react'
import { createContext, useContext } from 'react' import { createContext, useContext } from 'react'
import type { LivePreviewConfig } from '../../../../../exports/config' import type { LivePreviewConfig } from '../../../../../exports/config'
import type { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
import type { usePopupWindow } from '../usePopupWindow'
import type { SizeReducerAction } from './sizeReducer' import type { SizeReducerAction } from './sizeReducer'
export interface LivePreviewContextType { export interface LivePreviewContextType {
appIsReady: boolean
breakpoint: LivePreviewConfig['breakpoints'][number]['name'] breakpoint: LivePreviewConfig['breakpoints'][number]['name']
breakpoints: LivePreviewConfig['breakpoints'] breakpoints: LivePreviewConfig['breakpoints']
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
iframeHasLoaded: boolean iframeHasLoaded: boolean
iframeRef: React.RefObject<HTMLIFrameElement> iframeRef: React.RefObject<HTMLIFrameElement>
isPopupOpen: boolean
measuredDeviceSize: { measuredDeviceSize: {
height: number height: number
width: number width: number
} }
openPopupWindow: ReturnType<typeof usePopupWindow>['openPopupWindow']
popupRef?: React.MutableRefObject<Window | null>
previewWindowType: 'iframe' | 'popup'
setAppIsReady: (appIsReady: boolean) => void
setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void
setHeight: (height: number) => void setHeight: (height: number) => void
setIframeHasLoaded: (loaded: boolean) => void setIframeHasLoaded: (loaded: boolean) => void
setMeasuredDeviceSize: (size: { height: number; width: number }) => void setMeasuredDeviceSize: (size: { height: number; width: number }) => void
setPreviewWindowType: (previewWindowType: 'iframe' | 'popup') => void
setSize: Dispatch<SizeReducerAction> setSize: Dispatch<SizeReducerAction>
setToolbarPosition: (position: { x: number; y: number }) => void setToolbarPosition: (position: { x: number; y: number }) => void
setWidth: (width: number) => void setWidth: (width: number) => void
@@ -30,22 +40,31 @@ export interface LivePreviewContextType {
x: number x: number
y: number y: number
} }
url: string | undefined
zoom: number zoom: number
} }
export const LivePreviewContext = createContext<LivePreviewContextType>({ export const LivePreviewContext = createContext<LivePreviewContextType>({
appIsReady: false,
breakpoint: undefined, breakpoint: undefined,
breakpoints: undefined, breakpoints: undefined,
fieldSchemaJSON: undefined,
iframeHasLoaded: false, iframeHasLoaded: false,
iframeRef: undefined, iframeRef: undefined,
isPopupOpen: false,
measuredDeviceSize: { measuredDeviceSize: {
height: 0, height: 0,
width: 0, width: 0,
}, },
openPopupWindow: () => {},
popupRef: undefined,
previewWindowType: 'iframe',
setAppIsReady: () => {},
setBreakpoint: () => {}, setBreakpoint: () => {},
setHeight: () => {}, setHeight: () => {},
setIframeHasLoaded: () => {}, setIframeHasLoaded: () => {},
setMeasuredDeviceSize: () => {}, setMeasuredDeviceSize: () => {},
setPreviewWindowType: () => {},
setSize: () => {}, setSize: () => {},
setToolbarPosition: () => {}, setToolbarPosition: () => {},
setWidth: () => {}, setWidth: () => {},
@@ -58,6 +77,7 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
x: 0, x: 0,
y: 0, y: 0,
}, },
url: undefined,
zoom: 1, zoom: 1,
}) })

View File

@@ -1,39 +1,48 @@
import { DndContext } from '@dnd-kit/core' import { DndContext } from '@dnd-kit/core'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import type { LivePreviewConfig } from '../../../../../exports/config' import type { LivePreviewConfig } from '../../../../../exports/config'
import type { Field } from '../../../../../fields/config/types'
import type { EditViewProps } from '../../types' import type { EditViewProps } from '../../types'
import type { usePopupWindow } from '../usePopupWindow' import type { usePopupWindow } from '../usePopupWindow'
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
import { customCollisionDetection } from './collisionDetection' import { customCollisionDetection } from './collisionDetection'
import { LivePreviewContext } from './context' import { LivePreviewContext } from './context'
import { sizeReducer } from './sizeReducer' import { sizeReducer } from './sizeReducer'
export type ToolbarProviderProps = EditViewProps & { export type LivePreviewProviderProps = EditViewProps & {
appIsReady?: boolean
breakpoints?: LivePreviewConfig['breakpoints'] breakpoints?: LivePreviewConfig['breakpoints']
children: React.ReactNode children: React.ReactNode
deviceSize?: { deviceSize?: {
height: number height: number
width: number width: number
} }
popupState: ReturnType<typeof usePopupWindow> isPopupOpen?: boolean
openPopupWindow?: ReturnType<typeof usePopupWindow>['openPopupWindow']
popupRef?: React.MutableRefObject<Window>
url?: string url?: string
} }
export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => { export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = (props) => {
const { breakpoints, children } = props const { breakpoints, children, isPopupOpen, openPopupWindow, popupRef, url } = props
const [previewWindowType, setPreviewWindowType] = useState<'iframe' | 'popup'>('iframe')
const [appIsReady, setAppIsReady] = useState(false)
const iframeRef = React.useRef<HTMLIFrameElement>(null) const iframeRef = React.useRef<HTMLIFrameElement>(null)
const [iframeHasLoaded, setIframeHasLoaded] = React.useState(false) const [iframeHasLoaded, setIframeHasLoaded] = useState(false)
const [zoom, setZoom] = React.useState(1) const [zoom, setZoom] = useState(1)
const [position, setPosition] = React.useState({ x: 0, y: 0 }) const [position, setPosition] = useState({ x: 0, y: 0 })
const [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 }) const [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 })
const [measuredDeviceSize, setMeasuredDeviceSize] = React.useState({ const [measuredDeviceSize, setMeasuredDeviceSize] = useState({
height: 0, height: 0,
width: 0, width: 0,
}) })
@@ -41,6 +50,22 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
const [breakpoint, setBreakpoint] = const [breakpoint, setBreakpoint] =
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive') React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
const [fieldSchemaJSON] = useState(() => {
let fields: Field[]
if ('collection' in props) {
const { collection } = props
fields = collection.fields
}
if ('global' in props) {
const { global } = props
fields = global.fields
}
return fieldSchemaToJSON(fields)
})
// The toolbar needs to freely drag and drop around the page // The toolbar needs to freely drag and drop around the page
const handleDragEnd = (ev) => { const handleDragEnd = (ev) => {
// only update position if the toolbar is completely within the preview area // only update position if the toolbar is completely within the preview area
@@ -94,24 +119,70 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
} }
}, [breakpoint, breakpoints]) }, [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: MessageEvent) => {
const data = JSON.parse(event.data)
if (url.startsWith(event.origin) && data.type === 'payload-live-preview' && data.ready) {
setAppIsReady(true)
}
}
window.addEventListener('message', handleMessage)
return () => {
window.removeEventListener('message', handleMessage)
}
}, [url])
const handleWindowChange = useCallback(
(type: 'iframe' | 'popup') => {
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(() => {
if (!isPopupOpen) {
handleWindowChange('iframe')
}
}, [isPopupOpen, handleWindowChange])
return ( return (
<LivePreviewContext.Provider <LivePreviewContext.Provider
value={{ value={{
appIsReady,
breakpoint, breakpoint,
breakpoints, breakpoints,
fieldSchemaJSON,
iframeHasLoaded, iframeHasLoaded,
iframeRef, iframeRef,
isPopupOpen,
measuredDeviceSize, measuredDeviceSize,
openPopupWindow,
popupRef,
previewWindowType,
setAppIsReady,
setBreakpoint, setBreakpoint,
setHeight, setHeight,
setIframeHasLoaded, setIframeHasLoaded,
setMeasuredDeviceSize, setMeasuredDeviceSize,
setPreviewWindowType: handleWindowChange,
setSize, setSize,
setToolbarPosition: setPosition, setToolbarPosition: setPosition,
setWidth, setWidth,
setZoom, setZoom,
size, size,
toolbarPosition: position, toolbarPosition: position,
url,
zoom, zoom,
}} }}
> >

View File

@@ -1,14 +1,9 @@
import React, { useEffect, useState } from 'react' import React, { useEffect } from 'react'
import type { LivePreviewConfig } from '../../../../../exports/config'
import type { Field } from '../../../../../fields/config/types'
import type { EditViewProps } from '../../types' import type { EditViewProps } from '../../types'
import type { usePopupWindow } from '../usePopupWindow'
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
import { useAllFormFields } from '../../../forms/Form/context' import { useAllFormFields } from '../../../forms/Form/context'
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues' import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
import { LivePreviewProvider } from '../Context'
import { useLivePreviewContext } from '../Context/context' import { useLivePreviewContext } from '../Context/context'
import { DeviceContainer } from '../Device' import { DeviceContainer } from '../Device'
import { IFrame } from '../IFrame' import { IFrame } from '../IFrame'
@@ -17,93 +12,81 @@ import './index.scss'
const baseClass = 'live-preview-window' const baseClass = 'live-preview-window'
const Preview: React.FC< export const LivePreview: React.FC<EditViewProps> = (props) => {
EditViewProps & {
popupState: ReturnType<typeof usePopupWindow>
url?: string
}
> = (props) => {
const { const {
popupState: { isPopupOpen, popupHasLoaded, popupRef }, appIsReady,
iframeHasLoaded,
iframeRef,
popupRef,
previewWindowType,
setIframeHasLoaded,
url, url,
} = props } = useLivePreviewContext()
const { iframeHasLoaded, iframeRef, setIframeHasLoaded } = useLivePreviewContext() const { breakpoint, fieldSchemaJSON } = useLivePreviewContext()
const { breakpoint } = useLivePreviewContext() const prevWindowType =
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>()
const [fields] = useAllFormFields() const [fields] = useAllFormFields()
const [fieldSchemaJSON] = useState(() => {
let fields: Field[]
if ('collection' in props) {
const { collection } = props
fields = collection.fields
}
if ('global' in props) {
const { global } = props
fields = global.fields
}
return fieldSchemaToJSON(fields)
})
// The preview could either be an iframe embedded on the page // The preview could either be an iframe embedded on the page
// Or it could be a separate popup window // Or it could be a separate popup window
// We need to transmit data to both accordingly // We need to transmit data to both accordingly
useEffect(() => { useEffect(() => {
if (fields && window && 'postMessage' in window) { // For performance, do no reduce fields to values until after the iframe or popup has loaded
if (fields && window && 'postMessage' in window && appIsReady) {
const values = reduceFieldsToValues(fields, true) const values = reduceFieldsToValues(fields, true)
// TODO: only send `fieldSchemaToJSON` one time // To reduce on large `postMessage` payloads, only send `fieldSchemaToJSON` one time
// To do this, the underlying JS function maintains a cache of this value
// So we need to send it through each time the window type changes
// But only once per window type change, not on every render, because this is a potentially large obj
const shouldSendSchema =
!prevWindowType.current || prevWindowType.current !== previewWindowType
prevWindowType.current = previewWindowType
const message = JSON.stringify({ const message = JSON.stringify({
data: values, data: values,
fieldSchemaJSON, fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
type: 'payload-live-preview', type: 'payload-live-preview',
}) })
// external window // Post message to external popup window
if (isPopupOpen) { if (previewWindowType === 'popup' && popupRef.current) {
setIframeHasLoaded(false) popupRef.current.postMessage(message, url)
if (popupRef.current) {
popupRef.current.postMessage(message, url)
}
} }
// embedded iframe // Post message to embedded iframe
if (!isPopupOpen) { if (previewWindowType === 'iframe' && iframeRef.current) {
if (iframeHasLoaded && iframeRef.current) { iframeRef.current.contentWindow?.postMessage(message, url)
iframeRef.current.contentWindow?.postMessage(message, url)
}
} }
} }
}, [ }, [
fields, fields,
url, url,
iframeHasLoaded, iframeHasLoaded,
isPopupOpen, previewWindowType,
popupRef, popupRef,
popupHasLoaded, appIsReady,
iframeRef, iframeRef,
setIframeHasLoaded, setIframeHasLoaded,
fieldSchemaJSON, fieldSchemaJSON,
]) ])
if (!isPopupOpen) { if (previewWindowType === 'iframe') {
return ( return (
<div <div
className={[ className={[
baseClass, baseClass,
isPopupOpen && `${baseClass}--popup-open`,
breakpoint && breakpoint !== 'responsive' && `${baseClass}--has-breakpoint`, breakpoint && breakpoint !== 'responsive' && `${baseClass}--has-breakpoint`,
] ]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
> >
<div className={`${baseClass}__wrapper`}> <div className={`${baseClass}__wrapper`}>
<LivePreviewToolbar {...props} iframeRef={iframeRef} url={url} /> <LivePreviewToolbar {...props} />
<div className={`${baseClass}__main`}> <div className={`${baseClass}__main`}>
<DeviceContainer> <DeviceContainer>
<IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} /> <IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} />
@@ -114,29 +97,3 @@ const Preview: React.FC<
) )
} }
} }
export const LivePreview: React.FC<
EditViewProps & {
livePreviewConfig?: LivePreviewConfig
popupState: ReturnType<typeof usePopupWindow>
url?: string
}
> = (props) => {
const { livePreviewConfig, url } = props
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),
{
name: 'responsive',
height: '100%',
label: 'Responsive',
width: '100%',
},
]
return (
<LivePreviewProvider {...props} breakpoints={breakpoints} url={url}>
<Preview {...props} />
</LivePreviewProvider>
)
}

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import type { LivePreviewToolbarProps } from '..' import type { EditViewProps } from '../../../types'
import { X } from '../../../..' import { X } from '../../../..'
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink' import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
@@ -10,13 +10,9 @@ import './index.scss'
const baseClass = 'live-preview-toolbar-controls' const baseClass = 'live-preview-toolbar-controls'
export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => { export const ToolbarControls: React.FC<EditViewProps> = () => {
const { breakpoint, breakpoints, setBreakpoint, setZoom, zoom } = useLivePreviewContext() const { breakpoint, breakpoints, setBreakpoint, setPreviewWindowType, setZoom, url, zoom } =
useLivePreviewContext()
const {
popupState: { openPopupWindow },
url,
} = props
return ( return (
<div className={baseClass}> <div className={baseClass}>
@@ -57,7 +53,15 @@ export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
<option value={150}>150%</option> <option value={150}>150%</option>
<option value={200}>200%</option> <option value={200}>200%</option>
</select> </select>
<a className={`${baseClass}__external`} href={url} onClick={openPopupWindow} type="button"> <a
className={`${baseClass}__external`}
href={url}
onClick={(e) => {
e.preventDefault()
setPreviewWindowType('popup')
}}
type="button"
>
<ExternalLinkIcon /> <ExternalLinkIcon />
</a> </a>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { useDraggable } from '@dnd-kit/core' import { useDraggable } from '@dnd-kit/core'
import React from 'react' import React from 'react'
import type { ToolbarProviderProps } from '../Context' import type { EditViewProps } from '../../types'
import DragHandle from '../../../icons/Drag' import DragHandle from '../../../icons/Drag'
import { useLivePreviewContext } from '../Context/context' import { useLivePreviewContext } from '../Context/context'
@@ -10,11 +10,7 @@ import './index.scss'
const baseClass = 'live-preview-toolbar' const baseClass = 'live-preview-toolbar'
export type LivePreviewToolbarProps = Omit<ToolbarProviderProps, 'children'> & { const DraggableToolbar: React.FC<EditViewProps> = (props) => {
iframeRef: React.RefObject<HTMLIFrameElement>
}
const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
const { toolbarPosition } = useLivePreviewContext() const { toolbarPosition } = useLivePreviewContext()
const { attributes, listeners, setNodeRef, transform } = useDraggable({ const { attributes, listeners, setNodeRef, transform } = useDraggable({
@@ -50,7 +46,7 @@ const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
) )
} }
const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => { const StaticToolbar: React.FC<EditViewProps> = (props) => {
return ( return (
<div className={[baseClass, `${baseClass}--static`].join(' ')}> <div className={[baseClass, `${baseClass}--static`].join(' ')}>
<ToolbarControls {...props} /> <ToolbarControls {...props} />
@@ -59,7 +55,7 @@ const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
} }
export const LivePreviewToolbar: React.FC< export const LivePreviewToolbar: React.FC<
LivePreviewToolbarProps & { EditViewProps & {
draggable?: boolean draggable?: boolean
} }
> = (props) => { > = (props) => {

View File

@@ -17,47 +17,17 @@ import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale' import { useLocale } from '../../utilities/Locale'
import Meta from '../../utilities/Meta' import Meta from '../../utilities/Meta'
import { SetStepNav } from '../collections/Edit/SetStepNav' import { SetStepNav } from '../collections/Edit/SetStepNav'
import { LivePreviewProvider } from './Context'
import { useLivePreviewContext } from './Context/context'
import { LivePreview } from './Preview' import { LivePreview } from './Preview'
import './index.scss' import './index.scss'
import { usePopupWindow } from './usePopupWindow' import { usePopupWindow } from './usePopupWindow'
const baseClass = 'live-preview' const baseClass = 'live-preview'
export const LivePreviewView: React.FC<EditViewProps> = (props) => { const PreviewView: React.FC<EditViewProps> = (props) => {
const { i18n, t } = useTranslation('general') const { i18n, t } = useTranslation('general')
const config = useConfig() const { previewWindowType } = useLivePreviewContext()
const documentInfo = useDocumentInfo()
const locale = useLocale()
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
if ('collection' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.collection.admin.livePreview || {}),
}
}
if ('global' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.global.admin.livePreview || {}),
}
}
const url =
typeof livePreviewConfig?.url === 'function'
? livePreviewConfig?.url({
data: props?.data,
documentInfo,
locale,
})
: livePreviewConfig?.url
const popupState = usePopupWindow({
eventType: 'payload-live-preview',
url,
})
const { apiURL, data, permissions } = props const { apiURL, data, permissions } = props
@@ -113,14 +83,14 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
permissions={permissions} permissions={permissions}
/> />
<div <div
className={[baseClass, popupState?.isPopupOpen && `${baseClass}--detached`] className={[baseClass, previewWindowType === 'popup' && `${baseClass}--detached`]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
> >
<div <div
className={[ className={[
`${baseClass}__main`, `${baseClass}__main`,
popupState?.isPopupOpen && `${baseClass}__main--popup-open`, previewWindowType === 'popup' && `${baseClass}__main--popup-open`,
] ]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
@@ -148,13 +118,67 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
)} )}
</Gutter> </Gutter>
</div> </div>
<LivePreview <LivePreview {...props} />
{...props}
livePreviewConfig={livePreviewConfig}
popupState={popupState}
url={url}
/>
</div> </div>
</Fragment> </Fragment>
) )
} }
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
const config = useConfig()
const documentInfo = useDocumentInfo()
const locale = useLocale()
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
if ('collection' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.collection.admin.livePreview || {}),
}
}
if ('global' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.global.admin.livePreview || {}),
}
}
const url =
typeof livePreviewConfig?.url === 'function'
? livePreviewConfig?.url({
data: props?.data,
documentInfo,
locale,
})
: livePreviewConfig?.url
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),
{
name: 'responsive',
height: '100%',
label: 'Responsive',
width: '100%',
},
]
const { isPopupOpen, openPopupWindow, popupRef } = usePopupWindow({
eventType: 'payload-live-preview',
url,
})
return (
<LivePreviewProvider
{...props}
breakpoints={breakpoints}
isPopupOpen={isPopupOpen}
openPopupWindow={openPopupWindow}
popupRef={popupRef}
url={url}
>
<PreviewView {...props} />
</LivePreviewProvider>
)
}

View File

@@ -19,17 +19,14 @@ export const usePopupWindow = (props: {
url: string url: string
}): { }): {
isPopupOpen: boolean isPopupOpen: boolean
openPopupWindow: (e: React.MouseEvent<HTMLAnchorElement>) => void openPopupWindow: () => void
popupHasLoaded: boolean
popupRef?: React.MutableRefObject<Window | null> popupRef?: React.MutableRefObject<Window | null>
} => { } => {
const { eventType, onMessage, url } = props const { eventType, onMessage, url } = props
const isReceivingMessage = useRef(false) const isReceivingMessage = useRef(false)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [popupHasLoaded, setPopupHasLoaded] = useState(false)
const { serverURL } = useConfig() const { serverURL } = useConfig()
const popupRef = useRef<Window | null>(null) const popupRef = useRef<Window | null>(null)
const hasAttachedMessageListener = useRef(false)
// Optionally broadcast messages back out to the parent component // Optionally broadcast messages back out to the parent component
useEffect(() => { useEffect(() => {
@@ -65,8 +62,10 @@ export const usePopupWindow = (props: {
// Customize the size, position, and style of the popup window // Customize the size, position, and style of the popup window
const openPopupWindow = useCallback( const openPopupWindow = useCallback(
(e) => { (e?: MouseEvent) => {
e.preventDefault() if (e) {
e.preventDefault()
}
const features = { const features = {
height: 700, height: 700,
@@ -106,27 +105,6 @@ export const usePopupWindow = (props: {
[url], [url],
) )
// the only cross-origin way of detecting when a popup window has loaded
// we catch a message event that the site rendered within the popup window fires
// there is no way in js to add an event listener to a popup window across domains
useEffect(() => {
if (hasAttachedMessageListener.current) return
hasAttachedMessageListener.current = true
window.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
if (
url.startsWith(event.origin) &&
data.type === eventType &&
data.popupReady &&
!popupHasLoaded
) {
setPopupHasLoaded(true)
}
})
}, [url, eventType, popupHasLoaded])
// this is the most stable and widely supported way to check if a popup window is no longer open // this is the most stable and widely supported way to check if a popup window is no longer open
// we poll its ref every x ms and use the popup window's `closed` property // we poll its ref every x ms and use the popup window's `closed` property
useEffect(() => { useEffect(() => {
@@ -137,7 +115,6 @@ export const usePopupWindow = (props: {
if (popupRef.current.closed) { if (popupRef.current.closed) {
clearInterval(timer) clearInterval(timer)
setIsOpen(false) setIsOpen(false)
setPopupHasLoaded(false)
} }
}, 1000) }, 1000)
} else { } else {
@@ -154,7 +131,6 @@ export const usePopupWindow = (props: {
return { return {
isPopupOpen: isOpen, isPopupOpen: isOpen,
openPopupWindow, openPopupWindow,
popupHasLoaded,
popupRef, popupRef,
} }
} }

View File

@@ -0,0 +1,10 @@
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'title',
},
fields: [],
}

View File

@@ -1,20 +1,12 @@
import path from 'path'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials'
import Categories from './collections/Categories' import Categories from './collections/Categories'
import { Media } from './collections/Media' import { Media } from './collections/Media'
import { Pages } from './collections/Pages' import { Pages } from './collections/Pages'
import { Posts, postsSlug } from './collections/Posts' import { Posts } from './collections/Posts'
import { Users } from './collections/Users'
import { Footer } from './globals/Footer' import { Footer } from './globals/Footer'
import { Header } from './globals/Header' import { Header } from './globals/Header'
import { footer } from './seed/footer' import { seed } from './seed'
import { header } from './seed/header'
import { home } from './seed/home'
import { post1 } from './seed/post-1'
import { post2 } from './seed/post-2'
import { post3 } from './seed/post-3'
import { postsPage } from './seed/posts-page'
export const pagesSlug = 'pages' export const pagesSlug = 'pages'
@@ -40,92 +32,7 @@ export default buildConfigWithDefaults({
}, },
cors: ['http://localhost:3001'], cors: ['http://localhost:3001'],
csrf: ['http://localhost:3001'], csrf: ['http://localhost:3001'],
collections: [ collections: [Users, Pages, Posts, Categories, Media],
{
slug: 'users',
auth: true,
admin: {
useAsTitle: 'title',
},
fields: [],
},
Pages,
Posts,
Categories,
Media,
],
globals: [Header, Footer], globals: [Header, Footer],
onInit: async (payload) => { onInit: seed,
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
const media = await payload.create({
collection: 'media',
filePath: path.resolve(__dirname, 'image-1.jpg'),
data: {
alt: 'Image 1',
},
})
const mediaID = payload.db.defaultIDType === 'number' ? media.id : `"${media.id}"`
const [post1Doc, post2Doc, post3Doc] = await Promise.all([
await payload.create({
collection: postsSlug,
data: JSON.parse(JSON.stringify(post1).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
}),
await payload.create({
collection: postsSlug,
data: JSON.parse(JSON.stringify(post2).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
}),
await payload.create({
collection: postsSlug,
data: JSON.parse(JSON.stringify(post3).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
}),
])
const postsPageDoc = await payload.create({
collection: pagesSlug,
data: JSON.parse(JSON.stringify(postsPage).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
})
let postsPageDocID = postsPageDoc.id
let post1DocID = post1Doc.id
let post2DocID = post2Doc.id
let post3DocID = post3Doc.id
if (payload.db.defaultIDType !== 'number') {
postsPageDocID = `"${postsPageDoc.id}"`
post1DocID = `"${post1Doc.id}"`
post2DocID = `"${post2Doc.id}"`
post3DocID = `"${post3Doc.id}"`
}
await payload.create({
collection: pagesSlug,
data: JSON.parse(
JSON.stringify(home)
.replace(/"\{\{MEDIA_ID\}\}"/g, mediaID)
.replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)
.replace(/"\{\{POST_1_ID\}\}"/g, post1DocID)
.replace(/"\{\{POST_2_ID\}\}"/g, post2DocID)
.replace(/"\{\{POST_3_ID\}\}"/g, post3DocID),
),
})
await payload.updateGlobal({
slug: 'header',
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),
})
await payload.updateGlobal({
slug: 'footer',
data: JSON.parse(JSON.stringify(footer)),
})
},
}) })

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -0,0 +1,88 @@
import path from 'path'
import type { Config } from '../../../packages/payload/src/config/types'
import { devUser } from '../../credentials'
import { postsSlug } from '../collections/Posts'
import { pagesSlug } from '../config'
import { footer } from './footer'
import { header } from './header'
import { home } from './home'
import { post1 } from './post-1'
import { post2 } from './post-2'
import { post3 } from './post-3'
import { postsPage } from './posts-page'
export const seed: Config['onInit'] = async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
const media = await payload.create({
collection: 'media',
filePath: path.resolve(__dirname, 'image-1.jpg'),
data: {
alt: 'Image 1',
},
})
const mediaID = payload.db.defaultIDType === 'number' ? media.id : `"${media.id}"`
const [post1Doc, post2Doc, post3Doc] = await Promise.all([
await payload.create({
collection: postsSlug,
data: JSON.parse(JSON.stringify(post1).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
}),
await payload.create({
collection: postsSlug,
data: JSON.parse(JSON.stringify(post2).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
}),
await payload.create({
collection: postsSlug,
data: JSON.parse(JSON.stringify(post3).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
}),
])
const postsPageDoc = await payload.create({
collection: pagesSlug,
data: JSON.parse(JSON.stringify(postsPage).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
})
let postsPageDocID = postsPageDoc.id
let post1DocID = post1Doc.id
let post2DocID = post2Doc.id
let post3DocID = post3Doc.id
if (payload.db.defaultIDType !== 'number') {
postsPageDocID = `"${postsPageDoc.id}"`
post1DocID = `"${post1Doc.id}"`
post2DocID = `"${post2Doc.id}"`
post3DocID = `"${post3Doc.id}"`
}
await payload.create({
collection: pagesSlug,
data: JSON.parse(
JSON.stringify(home)
.replace(/"\{\{MEDIA_ID\}\}"/g, mediaID)
.replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)
.replace(/"\{\{POST_1_ID\}\}"/g, post1DocID)
.replace(/"\{\{POST_2_ID\}\}"/g, post2DocID)
.replace(/"\{\{POST_3_ID\}\}"/g, post3DocID),
),
})
await payload.updateGlobal({
slug: 'header',
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),
})
await payload.updateGlobal({
slug: 'footer',
data: JSON.parse(JSON.stringify(footer)),
})
}