sanity 3.83.1-canary.23 → 3.84.1-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/_chunks-cjs/PostMessageSchema.js.map +1 -1
- package/lib/_chunks-cjs/PostMessageTelemetry.js +1 -1
- package/lib/_chunks-cjs/PostMessageTelemetry.js.map +1 -1
- package/lib/_chunks-cjs/PresentationToolGrantsCheck.js +429 -1417
- package/lib/_chunks-cjs/PresentationToolGrantsCheck.js.map +1 -1
- package/lib/_chunks-cjs/presentation.js +70 -22
- package/lib/_chunks-cjs/presentation.js.map +1 -1
- package/lib/_chunks-cjs/resources6.js +0 -8
- package/lib/_chunks-cjs/resources6.js.map +1 -1
- package/lib/_chunks-cjs/version.js +1 -1
- package/lib/_chunks-es/PostMessageSchema.mjs.map +1 -1
- package/lib/_chunks-es/PostMessageTelemetry.mjs +1 -1
- package/lib/_chunks-es/PostMessageTelemetry.mjs.map +1 -1
- package/lib/_chunks-es/PresentationToolGrantsCheck.mjs +437 -1406
- package/lib/_chunks-es/PresentationToolGrantsCheck.mjs.map +1 -1
- package/lib/_chunks-es/presentation.mjs +73 -24
- package/lib/_chunks-es/presentation.mjs.map +1 -1
- package/lib/_chunks-es/resources6.mjs +0 -8
- package/lib/_chunks-es/resources6.mjs.map +1 -1
- package/lib/_chunks-es/version.mjs +1 -1
- package/lib/_singletons.d.mts +61 -2889
- package/lib/_singletons.d.ts +61 -2889
- package/lib/presentation.d.mts +57 -2887
- package/lib/presentation.d.ts +57 -2887
- package/lib/presentation.js +4 -0
- package/lib/presentation.js.map +1 -1
- package/lib/presentation.mjs +5 -1
- package/package.json +15 -16
- package/src/presentation/PostMessageTelemetry.tsx +1 -1
- package/src/presentation/PresentationTool.tsx +76 -85
- package/src/presentation/PresentationToolGrantsCheck.tsx +75 -39
- package/src/presentation/document/LocationsBanner.tsx +13 -52
- package/src/presentation/i18n/resources.ts +0 -10
- package/src/presentation/index.ts +13 -0
- package/src/presentation/{util → lib}/parse.ts +1 -2
- package/src/presentation/overlays/schema/PostMessageSchema.tsx +0 -1
- package/src/presentation/panels/usePanelsStorage.ts +1 -1
- package/src/presentation/preview/IFrame.tsx +35 -83
- package/src/presentation/preview/Preview.tsx +56 -172
- package/src/presentation/preview/PreviewHeader.tsx +10 -43
- package/src/presentation/preview/PreviewLocationInput.tsx +24 -48
- package/src/presentation/preview/SharePreviewMenu.tsx +2 -2
- package/src/presentation/reducers/presentationReducer.ts +134 -0
- package/src/presentation/types.ts +7 -139
- package/src/presentation/useMainDocument.ts +12 -4
- package/src/presentation/useParams.ts +3 -2
- package/src/presentation/usePreviewUrl.ts +133 -0
- package/src/presentation/actors/create-preview-secret.ts +0 -19
- package/src/presentation/actors/read-shared-secret.ts +0 -18
- package/src/presentation/actors/resolve-allow-patterns.ts +0 -55
- package/src/presentation/actors/resolve-initial-url.ts +0 -65
- package/src/presentation/actors/resolve-preview-mode-url.ts +0 -72
- package/src/presentation/actors/resolve-preview-mode.ts +0 -66
- package/src/presentation/actors/resolve-url-from-preview-search-param.ts +0 -29
- package/src/presentation/machines/presentation-machine.ts +0 -101
- package/src/presentation/machines/preview-url.ts +0 -568
- package/src/presentation/useAllowPatterns.ts +0 -12
- package/src/presentation/useId.ts +0 -7
- package/src/presentation/usePresentationPerspective.ts +0 -14
- package/src/presentation/usePreviewUrlActorRef.ts +0 -96
- package/src/presentation/useReportInvalidPreviewSearchParam.tsx +0 -43
- package/src/presentation/useTargetOrigin.ts +0 -11
- /package/src/presentation/{util → lib}/debounce.ts +0 -0
@@ -110,26 +110,19 @@ export function LocationsBanner(props: {
|
|
110
110
|
</Flex>
|
111
111
|
</Card>
|
112
112
|
<Stack hidden={!expanded} marginTop={1} space={1}>
|
113
|
-
{locations.map((l) =>
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
documentType={schemaType.name}
|
127
|
-
key={l.href}
|
128
|
-
node={l}
|
129
|
-
toolName={options.name || DEFAULT_TOOL_NAME}
|
130
|
-
/>
|
131
|
-
)
|
132
|
-
})}
|
113
|
+
{locations.map((l, index) => (
|
114
|
+
<LocationItem
|
115
|
+
active={
|
116
|
+
(options.name || DEFAULT_TOOL_NAME) === presentationName &&
|
117
|
+
l.href === presentation?.params.preview
|
118
|
+
}
|
119
|
+
documentId={documentId}
|
120
|
+
documentType={schemaType.name}
|
121
|
+
key={index}
|
122
|
+
node={l}
|
123
|
+
toolName={options.name || DEFAULT_TOOL_NAME}
|
124
|
+
/>
|
125
|
+
))}
|
133
126
|
</Stack>
|
134
127
|
</>
|
135
128
|
)}
|
@@ -196,35 +189,3 @@ function LocationItem(props: {
|
|
196
189
|
</Card>
|
197
190
|
)
|
198
191
|
}
|
199
|
-
|
200
|
-
/**
|
201
|
-
* Compares two URLs to determine if they match based on origin, pathname, and search parameters
|
202
|
-
* The previewUrl should have all the search parameters that are in the locationUrl
|
203
|
-
*/
|
204
|
-
function areUrlsMatching(previewUrlString: string, locationUrlString: string): boolean {
|
205
|
-
try {
|
206
|
-
const previewUrl = new URL(previewUrlString, location.origin)
|
207
|
-
const locationUrl = new URL(locationUrlString, previewUrl.origin)
|
208
|
-
|
209
|
-
// First compare origin and pathname
|
210
|
-
if (previewUrl.origin !== locationUrl.origin || previewUrl.pathname !== locationUrl.pathname) {
|
211
|
-
return false
|
212
|
-
}
|
213
|
-
|
214
|
-
// Then check search params
|
215
|
-
// All search params in locationUrl must exist with the same values in previewUrl
|
216
|
-
const locationParams = new URLSearchParams(locationUrl.search)
|
217
|
-
const previewParams = new URLSearchParams(previewUrl.search)
|
218
|
-
|
219
|
-
for (const [key, value] of locationParams.entries()) {
|
220
|
-
if (previewParams.get(key) !== value) {
|
221
|
-
return false
|
222
|
-
}
|
223
|
-
}
|
224
|
-
|
225
|
-
return true
|
226
|
-
} catch {
|
227
|
-
// If URL parsing fails, URLs don't match
|
228
|
-
return false
|
229
|
-
}
|
230
|
-
}
|
@@ -35,16 +35,6 @@ export default {
|
|
35
35
|
'presentation-error.label': 'Error message',
|
36
36
|
/** The text shown when the preview frame cannot connect to Presentation */
|
37
37
|
'preview-frame.connection.error.text': 'Could not connect to the preview',
|
38
|
-
/** The title of the error toast that shows when the preview iframe times out while waiting for a connection over comlink to establish, and the root cause is discovered to be that the iframe is loading on an URL origin that's not in the allow list. */
|
39
|
-
'preview-frame.configuration.error.title': 'Preview URL origin mismatch',
|
40
|
-
/** The toast description for the error message explaining that the iframe is blocked from loading due to a security mismatch. */
|
41
|
-
'preview-frame.configuration.error.description':
|
42
|
-
'The preview iframe is configured to load <Code>{{targetOrigin}}</Code>, but the reported origin is <Code>{{reportedOrigin}}</Code>. Presentation Tool is unable to connect to unknown origins for security purposes. Update your <Code>presentationTool.allowOrigins</Code> configuration to allow connecting to Visual Editing and Loaders.',
|
43
|
-
/** The title of the error toast that shows when attempting to navigate to a preview URL origin that's not in the allow list. */
|
44
|
-
'preview-search-param.configuration.error.title': 'Blocked preview URL',
|
45
|
-
/** The toast description for the error message explaining that the iframe won't navigate to the new preview URL due to an URL origin security mismatch. */
|
46
|
-
'preview-search-param.configuration.error.description':
|
47
|
-
'The router wants to navigate to <Code>{{previewSearchParam}}</Code>, but the origin <Code>{{blockedOrigin}}</Code> is not allowed. Update your <Code>presentationTool.allowOrigins</Code> configuration to allow it.',
|
48
38
|
/** The text shown on the button for dismissing the error overlay after a timeout */
|
49
39
|
'preview-frame.continue-button.text': 'Continue anyway',
|
50
40
|
/** The label for the loader's connection status */
|
@@ -2,6 +2,19 @@ export {useSharedState} from './overlays/useSharedState'
|
|
2
2
|
export * from './plugin'
|
3
3
|
export type {PreviewProps} from './preview/Preview'
|
4
4
|
export type {PreviewHeaderProps} from './preview/PreviewHeader'
|
5
|
+
export {
|
6
|
+
ACTION_IFRAME_LOADED,
|
7
|
+
ACTION_IFRAME_REFRESH,
|
8
|
+
ACTION_IFRAME_RELOAD,
|
9
|
+
ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE,
|
10
|
+
type DispatchPresentationAction,
|
11
|
+
type IframeLoadedAction,
|
12
|
+
type IframeRefreshAction,
|
13
|
+
type IframeReloadAction,
|
14
|
+
type PresentationAction,
|
15
|
+
type PresentationState,
|
16
|
+
type VisualEditingOverlaysToggleAction,
|
17
|
+
} from './reducers/presentationReducer'
|
5
18
|
export type {
|
6
19
|
CombinedSearchParams,
|
7
20
|
ConnectionStatus,
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import {studioPath} from '@sanity/client/csm'
|
2
2
|
import {urlStringToPath} from '@sanity/visual-editing-csm'
|
3
|
-
|
4
|
-
import {type PresentationStateParams} from '../types'
|
3
|
+
import type {PresentationStateParams} from '../types'
|
5
4
|
|
6
5
|
export function parseId(rawId: string | undefined): string | undefined {
|
7
6
|
if (rawId === undefined) {
|
@@ -75,7 +75,6 @@ function PostMessageSchema(props: PostMessageSchemaProps): React.JSX.Element | n
|
|
75
75
|
const arr = Array.from(paths)
|
76
76
|
const projection = arr.map((path, i) => `"${i}": ${path}[0]._type`).join(',')
|
77
77
|
const query = `*[_id == $id][0]{${projection}}`
|
78
|
-
// Should implement max 25 concurrent queries here
|
79
78
|
const result = await client.fetch(
|
80
79
|
query,
|
81
80
|
{id: getPublishedId(id)},
|
@@ -1,75 +1,7 @@
|
|
1
1
|
import {Box} from '@sanity/ui'
|
2
2
|
import {motion, type VariantLabels, type Variants} from 'framer-motion'
|
3
|
-
import {forwardRef, type ReactEventHandler,
|
4
|
-
import {
|
5
|
-
|
6
|
-
import {useId} from '../useId'
|
7
|
-
|
8
|
-
interface IFrameProps {
|
9
|
-
animate: VariantLabels
|
10
|
-
initial: VariantLabels
|
11
|
-
onLoad: ReactEventHandler<HTMLIFrameElement>
|
12
|
-
preventClick: boolean
|
13
|
-
src: string
|
14
|
-
variants: Variants
|
15
|
-
style: React.CSSProperties
|
16
|
-
}
|
17
|
-
|
18
|
-
export const IFrame = forwardRef<HTMLIFrameElement, IFrameProps>(
|
19
|
-
function IFrame(props, forwardedRef) {
|
20
|
-
const {animate, initial, onLoad, preventClick, src, variants, style} = props
|
21
|
-
|
22
|
-
const ref = useRef<HTMLIFrameElement | null>(null)
|
23
|
-
// Forward the iframe ref to the parent component
|
24
|
-
useImperativeHandle<HTMLIFrameElement | null, HTMLIFrameElement | null>(
|
25
|
-
forwardedRef,
|
26
|
-
() => ref.current,
|
27
|
-
)
|
28
|
-
|
29
|
-
/**
|
30
|
-
* Ensure that clicking outside of menus and dialogs will close as focus shifts to the iframe
|
31
|
-
*/
|
32
|
-
|
33
|
-
useEffect(() => {
|
34
|
-
if (!ref.current) {
|
35
|
-
return undefined
|
36
|
-
}
|
37
|
-
const instance = ref.current
|
38
|
-
function handleBlur() {
|
39
|
-
if (instance !== document.activeElement) {
|
40
|
-
return
|
41
|
-
}
|
42
|
-
|
43
|
-
instance.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))
|
44
|
-
}
|
45
|
-
window.addEventListener('blur', handleBlur)
|
46
|
-
return () => {
|
47
|
-
window.removeEventListener('blur', handleBlur)
|
48
|
-
}
|
49
|
-
}, [])
|
50
|
-
|
51
|
-
const viewTransitionName = useId()
|
52
|
-
|
53
|
-
return (
|
54
|
-
<>
|
55
|
-
<IFrameElement
|
56
|
-
style={{
|
57
|
-
...style,
|
58
|
-
viewTransitionName,
|
59
|
-
}}
|
60
|
-
animate={animate}
|
61
|
-
initial={initial}
|
62
|
-
onLoad={onLoad}
|
63
|
-
ref={ref}
|
64
|
-
src={src}
|
65
|
-
variants={variants}
|
66
|
-
/>
|
67
|
-
{preventClick && <IFrameOverlay />}
|
68
|
-
<GlobalViewTransition />
|
69
|
-
</>
|
70
|
-
)
|
71
|
-
},
|
72
|
-
)
|
3
|
+
import {forwardRef, type ReactEventHandler, useId} from 'react'
|
4
|
+
import {styled} from 'styled-components'
|
73
5
|
|
74
6
|
const IFrameElement = motion.create(styled.iframe`
|
75
7
|
box-shadow: 0 0 0 1px var(--card-border-color);
|
@@ -85,17 +17,37 @@ const IFrameOverlay = styled(Box)`
|
|
85
17
|
background: transparent;
|
86
18
|
`
|
87
19
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
}
|
97
|
-
&::view-transition-new(root) {
|
98
|
-
animation: none;
|
99
|
-
} */
|
20
|
+
interface IFrameProps {
|
21
|
+
animate: VariantLabels
|
22
|
+
initial: VariantLabels
|
23
|
+
onLoad: ReactEventHandler<HTMLIFrameElement>
|
24
|
+
preventClick: boolean
|
25
|
+
src: string
|
26
|
+
variants: Variants
|
27
|
+
style: React.CSSProperties
|
100
28
|
}
|
101
|
-
|
29
|
+
|
30
|
+
export const IFrame = forwardRef<HTMLIFrameElement, IFrameProps>(function IFrame(props, ref) {
|
31
|
+
const {animate, initial, onLoad, preventClick, src, variants, style} = props
|
32
|
+
const id = useId()
|
33
|
+
|
34
|
+
return (
|
35
|
+
<>
|
36
|
+
<IFrameElement
|
37
|
+
style={{
|
38
|
+
...style,
|
39
|
+
// useId() guarantees that the ID will be unique, even if we add support for multiple iframe instances,
|
40
|
+
// while `view-transition-class: presentation-tool-iframe` provides userland a way to customize the transition with CSS if they wish
|
41
|
+
viewTransitionName: `presentation-tool-iframe-${id.replace(/[^a-zA-Z0-9-_]/g, '_')}`,
|
42
|
+
}}
|
43
|
+
animate={animate}
|
44
|
+
initial={initial}
|
45
|
+
onLoad={onLoad}
|
46
|
+
ref={ref}
|
47
|
+
src={src}
|
48
|
+
variants={variants}
|
49
|
+
/>
|
50
|
+
{preventClick && <IFrameOverlay />}
|
51
|
+
</>
|
52
|
+
)
|
53
|
+
})
|
@@ -1,28 +1,11 @@
|
|
1
1
|
/* eslint-disable react/no-unused-prop-types,no-nested-ternary */
|
2
|
-
import {createConnectionMachine, createController} from '@sanity/comlink'
|
3
|
-
import {
|
4
|
-
createCompatibilityActors,
|
5
|
-
type VisualEditingControllerMsg,
|
6
|
-
type VisualEditingNodeMsg,
|
7
|
-
} from '@sanity/presentation-comlink'
|
8
2
|
import {
|
9
3
|
urlSearchParamPreviewPerspective,
|
10
4
|
urlSearchParamVercelProtectionBypass,
|
11
5
|
urlSearchParamVercelSetBypassCookie,
|
12
6
|
type VercelSetBypassCookieValue,
|
13
7
|
} from '@sanity/preview-url-secret/constants'
|
14
|
-
import {
|
15
|
-
Card,
|
16
|
-
Code,
|
17
|
-
Flex,
|
18
|
-
Label,
|
19
|
-
Spinner,
|
20
|
-
Stack,
|
21
|
-
Text,
|
22
|
-
usePrefersReducedMotion,
|
23
|
-
useToast,
|
24
|
-
} from '@sanity/ui'
|
25
|
-
import {useSelector} from '@xstate/react'
|
8
|
+
import {Card, Code, Flex, Label, Spinner, Stack, Text, usePrefersReducedMotion} from '@sanity/ui'
|
26
9
|
import {AnimatePresence, motion, MotionConfig} from 'framer-motion'
|
27
10
|
import {
|
28
11
|
forwardRef,
|
@@ -36,23 +19,24 @@ import {
|
|
36
19
|
useSyncExternalStore,
|
37
20
|
} from 'react'
|
38
21
|
import {flushSync} from 'react-dom'
|
39
|
-
import {
|
40
|
-
import {useEffectEvent} from 'use-effect-event'
|
22
|
+
import {useTranslation} from 'sanity'
|
41
23
|
|
42
24
|
import {Button, TooltipDelayGroupProvider} from '../../ui-components'
|
43
25
|
import {ErrorCard} from '../components/ErrorCard'
|
44
26
|
import {MAX_TIME_TO_OVERLAYS_CONNECTION} from '../constants'
|
45
27
|
import {presentationLocaleNamespace} from '../i18n'
|
46
|
-
import {
|
47
|
-
|
28
|
+
import {
|
29
|
+
ACTION_IFRAME_LOADED,
|
30
|
+
ACTION_IFRAME_RELOAD,
|
31
|
+
type DispatchPresentationAction,
|
32
|
+
type PresentationState,
|
33
|
+
} from '../reducers/presentationReducer'
|
48
34
|
import {
|
49
35
|
type ConnectionStatus,
|
50
36
|
type HeaderOptions,
|
51
37
|
type PresentationPerspective,
|
52
38
|
type PresentationViewport,
|
53
39
|
} from '../types'
|
54
|
-
import {useAllowPatterns} from '../useAllowPatterns'
|
55
|
-
import {usePresentationNavigate} from '../usePresentationNavigate'
|
56
40
|
import {usePresentationTool} from '../usePresentationTool'
|
57
41
|
import {encodeStudioPerspective} from '../util/encodeStudioPerspective'
|
58
42
|
import {IFrame} from './IFrame'
|
@@ -61,10 +45,11 @@ import {PreviewHeader} from './PreviewHeader'
|
|
61
45
|
const MotionFlex = motion.create(Flex)
|
62
46
|
|
63
47
|
/** @public */
|
64
|
-
export interface PreviewProps {
|
48
|
+
export interface PreviewProps extends Pick<PresentationState, 'iframe' | 'visualEditing'> {
|
65
49
|
canSharePreviewAccess: boolean
|
66
50
|
canToggleSharePreviewAccess: boolean
|
67
51
|
canUseSharedPreviewAccess: boolean
|
52
|
+
dispatch: DispatchPresentationAction
|
68
53
|
header?: HeaderOptions
|
69
54
|
initialUrl: URL
|
70
55
|
loadersConnection: ConnectionStatus
|
@@ -73,7 +58,6 @@ export interface PreviewProps {
|
|
73
58
|
onRefresh: (fallback: () => void) => void
|
74
59
|
openPopup: (url: string) => void
|
75
60
|
overlaysConnection: ConnectionStatus
|
76
|
-
presentationRef: PresentationMachineRef
|
77
61
|
perspective: PresentationPerspective
|
78
62
|
previewUrl?: string
|
79
63
|
setViewport: (mode: 'desktop' | 'mobile') => void
|
@@ -82,12 +66,13 @@ export interface PreviewProps {
|
|
82
66
|
toggleOverlay: () => void
|
83
67
|
viewport: PresentationViewport
|
84
68
|
vercelProtectionBypass: string | null
|
85
|
-
previewUrlRef: PreviewUrlRef
|
86
69
|
}
|
87
70
|
|
88
71
|
export const Preview = memo(
|
89
72
|
forwardRef<HTMLIFrameElement, PreviewProps>(function PreviewComponent(props, forwardedRef) {
|
90
73
|
const {
|
74
|
+
dispatch,
|
75
|
+
iframe,
|
91
76
|
header,
|
92
77
|
initialUrl,
|
93
78
|
loadersConnection,
|
@@ -95,8 +80,6 @@ export const Preview = memo(
|
|
95
80
|
perspective,
|
96
81
|
viewport,
|
97
82
|
vercelProtectionBypass,
|
98
|
-
presentationRef,
|
99
|
-
previewUrlRef,
|
100
83
|
} = props
|
101
84
|
|
102
85
|
const [stablePerspective, setStablePerspective] = useState<typeof perspective | null>(null)
|
@@ -150,17 +133,11 @@ export const Preview = memo(
|
|
150
133
|
() => ref.current,
|
151
134
|
)
|
152
135
|
|
153
|
-
const
|
154
|
-
presentationRef,
|
155
|
-
(state) => state.matches('loading') || state.matches({loaded: 'reloading'}),
|
156
|
-
)
|
157
|
-
|
136
|
+
const loading = iframe.status === 'loading' || iframe.status === 'reloading'
|
158
137
|
const [timedOut, setTimedOut] = useState(false)
|
159
|
-
const
|
160
|
-
state.matches({loaded: 'refreshing'}),
|
161
|
-
)
|
138
|
+
const refreshing = iframe.status === 'refreshing'
|
162
139
|
const [somethingIsWrong, setSomethingIsWrong] = useState(false)
|
163
|
-
const iframeIsBusy =
|
140
|
+
const iframeIsBusy = loading || refreshing || overlaysConnection === 'connecting'
|
164
141
|
|
165
142
|
const handleRetry = useCallback(() => {
|
166
143
|
if (!ref.current) {
|
@@ -169,8 +146,8 @@ export const Preview = memo(
|
|
169
146
|
|
170
147
|
ref.current.src = previewUrl.toString()
|
171
148
|
|
172
|
-
|
173
|
-
}, [
|
149
|
+
dispatch({type: ACTION_IFRAME_RELOAD})
|
150
|
+
}, [dispatch, previewUrl])
|
174
151
|
const handleContinueAnyway = useCallback(() => {
|
175
152
|
setContinueAnyway(true)
|
176
153
|
}, [])
|
@@ -178,7 +155,7 @@ export const Preview = memo(
|
|
178
155
|
const [continueAnyway, setContinueAnyway] = useState(false)
|
179
156
|
const [showOverlaysConnectionStatus, setShowOverlaysConnectionState] = useState(false)
|
180
157
|
useEffect(() => {
|
181
|
-
if (
|
158
|
+
if (loading || refreshing) {
|
182
159
|
return undefined
|
183
160
|
}
|
184
161
|
|
@@ -189,10 +166,10 @@ export const Preview = memo(
|
|
189
166
|
return () => clearTimeout(timeout)
|
190
167
|
}
|
191
168
|
return undefined
|
192
|
-
}, [overlaysConnection,
|
169
|
+
}, [overlaysConnection, loading, refreshing])
|
193
170
|
|
194
171
|
useEffect(() => {
|
195
|
-
if (
|
172
|
+
if (loading || refreshing || !showOverlaysConnectionStatus) {
|
196
173
|
return undefined
|
197
174
|
}
|
198
175
|
if (overlaysConnection === 'connected') {
|
@@ -219,17 +196,39 @@ export const Preview = memo(
|
|
219
196
|
return () => clearTimeout(timeout)
|
220
197
|
}
|
221
198
|
return undefined
|
222
|
-
}, [
|
199
|
+
}, [loading, overlaysConnection, refreshing, showOverlaysConnectionStatus])
|
223
200
|
|
224
201
|
const onIFrameLoad = useCallback(() => {
|
225
|
-
|
226
|
-
}, [
|
202
|
+
dispatch({type: ACTION_IFRAME_LOADED})
|
203
|
+
}, [dispatch])
|
204
|
+
|
205
|
+
/**
|
206
|
+
* Ensure that clicking outside of menus and dialogs will close as focus shifts to the iframe
|
207
|
+
*/
|
208
|
+
useEffect(() => {
|
209
|
+
if (!ref.current) {
|
210
|
+
return undefined
|
211
|
+
}
|
212
|
+
const instance = ref.current
|
213
|
+
function handleBlur() {
|
214
|
+
if (instance !== document.activeElement) {
|
215
|
+
return
|
216
|
+
}
|
217
|
+
|
218
|
+
instance.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))
|
219
|
+
}
|
220
|
+
window.addEventListener('blur', handleBlur)
|
221
|
+
return () => {
|
222
|
+
window.removeEventListener('blur', handleBlur)
|
223
|
+
}
|
224
|
+
}, [])
|
227
225
|
|
228
226
|
const preventIframeInteraction = useMemo(() => {
|
229
227
|
return (
|
230
|
-
(
|
228
|
+
(loading || (overlaysConnection === 'connecting' && iframe.status !== 'refreshing')) &&
|
229
|
+
!continueAnyway
|
231
230
|
)
|
232
|
-
}, [continueAnyway,
|
231
|
+
}, [continueAnyway, iframe.status, loading, overlaysConnection])
|
233
232
|
|
234
233
|
const canUseViewTransition = useSyncExternalStore(
|
235
234
|
// eslint-disable-next-line no-empty-function
|
@@ -239,7 +238,7 @@ export const Preview = memo(
|
|
239
238
|
const iframeAnimations = useMemo(() => {
|
240
239
|
return [
|
241
240
|
preventIframeInteraction ? 'background' : 'active',
|
242
|
-
|
241
|
+
loading ? 'reloading' : 'idle',
|
243
242
|
// If CSS View Transitions are supported, then transition iframe viewport dimensions with that instead of Motion
|
244
243
|
canUseViewTransition ? '' : viewport,
|
245
244
|
showOverlaysConnectionStatus && !continueAnyway ? 'timedOut' : '',
|
@@ -247,7 +246,7 @@ export const Preview = memo(
|
|
247
246
|
}, [
|
248
247
|
canUseViewTransition,
|
249
248
|
continueAnyway,
|
250
|
-
|
249
|
+
loading,
|
251
250
|
preventIframeInteraction,
|
252
251
|
showOverlaysConnectionStatus,
|
253
252
|
viewport,
|
@@ -266,135 +265,19 @@ export const Preview = memo(
|
|
266
265
|
'startViewTransition' in document &&
|
267
266
|
typeof document.startViewTransition === 'function'
|
268
267
|
) {
|
269
|
-
document.startViewTransition(
|
270
|
-
// @ts-expect-error - fix typings
|
271
|
-
update: () => flushSync(() => update()),
|
272
|
-
types: ['sanity-iframe-viewport'],
|
273
|
-
})
|
268
|
+
document.startViewTransition(() => flushSync(() => update()))
|
274
269
|
} else {
|
275
270
|
update()
|
276
271
|
}
|
277
272
|
}
|
278
273
|
}, [canUseViewTransition, prefersReducedMotion, currentViewport, viewport])
|
279
274
|
|
280
|
-
const toast = useToast()
|
281
|
-
const allowOrigins = useAllowPatterns(previewUrlRef)
|
282
|
-
const [checkOrigin, setCheckOrigin] = useState<false | string>(false)
|
283
|
-
const [reportedMismatches] = useState(new Set<string>())
|
284
|
-
const reportMismatchingOrigin = useEffectEvent((reportedOrigin: string) => {
|
285
|
-
if (allowOrigins.some((allow) => allow.test(reportedOrigin))) {
|
286
|
-
setCheckOrigin(reportedOrigin)
|
287
|
-
return
|
288
|
-
}
|
289
|
-
if (reportedMismatches.has(reportedOrigin)) return
|
290
|
-
reportedMismatches.add(reportedOrigin)
|
291
|
-
console.warn('Visual Editing is here but misconfigured', {reportedOrigin})
|
292
|
-
toast.push({
|
293
|
-
closable: true,
|
294
|
-
id: `presentation-iframe-origin-mismatch-${reportedOrigin}`,
|
295
|
-
status: 'error',
|
296
|
-
duration: Infinity,
|
297
|
-
title: t('preview-frame.configuration.error.title'),
|
298
|
-
description: (
|
299
|
-
<Translate
|
300
|
-
t={t}
|
301
|
-
i18nKey="preview-frame.configuration.error.description"
|
302
|
-
components={{Code: 'code'}}
|
303
|
-
values={{
|
304
|
-
targetOrigin: previewUrl.origin,
|
305
|
-
reportedOrigin,
|
306
|
-
}}
|
307
|
-
/>
|
308
|
-
),
|
309
|
-
})
|
310
|
-
})
|
311
|
-
const navigate = usePresentationNavigate()
|
312
|
-
const navigateEvent = useEffectEvent((url: string) => {
|
313
|
-
if (!checkOrigin) return
|
314
|
-
const nextUrl = new URL(url, checkOrigin)
|
315
|
-
navigate(`${checkOrigin}${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`)
|
316
|
-
})
|
317
|
-
useEffect(() => {
|
318
|
-
if (!checkOrigin) {
|
319
|
-
return undefined
|
320
|
-
}
|
321
|
-
const target = ref.current?.contentWindow
|
322
|
-
if (!target) {
|
323
|
-
return undefined
|
324
|
-
}
|
325
|
-
const controller = createController({targetOrigin: checkOrigin})
|
326
|
-
controller.addTarget(target)
|
327
|
-
const comlink = controller.createChannel<VisualEditingControllerMsg, VisualEditingNodeMsg>(
|
328
|
-
{
|
329
|
-
name: 'presentation',
|
330
|
-
heartbeat: true,
|
331
|
-
connectTo: 'visual-editing',
|
332
|
-
},
|
333
|
-
createConnectionMachine<VisualEditingControllerMsg, VisualEditingNodeMsg>().provide({
|
334
|
-
actors: createCompatibilityActors<VisualEditingControllerMsg>(),
|
335
|
-
}),
|
336
|
-
)
|
337
|
-
|
338
|
-
comlink.on('visual-editing/navigate', (data) => {
|
339
|
-
navigateEvent(data.url)
|
340
|
-
})
|
341
|
-
const stop = comlink.start()
|
342
|
-
|
343
|
-
return () => {
|
344
|
-
stop()
|
345
|
-
controller.destroy()
|
346
|
-
}
|
347
|
-
}, [checkOrigin])
|
348
|
-
useEffect(() => {
|
349
|
-
if (overlaysConnection === 'connecting' || overlaysConnection === 'reconnecting') {
|
350
|
-
const interval = setInterval(() => {
|
351
|
-
ref.current?.contentWindow?.postMessage(
|
352
|
-
{domain: 'sanity/channels', from: 'presentation', type: 'presentation/status'},
|
353
|
-
/**
|
354
|
-
* The targetOrigin is set to '*' intentionally here, as we need to find out if the iframe is misconfigured and has the wrong origin
|
355
|
-
*/
|
356
|
-
'*',
|
357
|
-
)
|
358
|
-
}, 1_000)
|
359
|
-
|
360
|
-
const controller = new AbortController()
|
361
|
-
window.addEventListener(
|
362
|
-
'message',
|
363
|
-
({data}: MessageEvent<unknown>) => {
|
364
|
-
/**
|
365
|
-
* Listen for replies to presentation/status
|
366
|
-
*/
|
367
|
-
if (
|
368
|
-
data &&
|
369
|
-
typeof data === 'object' &&
|
370
|
-
'domain' in data &&
|
371
|
-
data.domain === 'sanity/channels' &&
|
372
|
-
'type' in data &&
|
373
|
-
data.type === 'visual-editing/status' &&
|
374
|
-
'data' in data &&
|
375
|
-
typeof data.data === 'object' &&
|
376
|
-
data.data &&
|
377
|
-
'origin' in data.data &&
|
378
|
-
typeof data.data.origin === 'string'
|
379
|
-
) {
|
380
|
-
reportMismatchingOrigin(data.data.origin)
|
381
|
-
}
|
382
|
-
},
|
383
|
-
{signal: controller.signal},
|
384
|
-
)
|
385
|
-
|
386
|
-
return () => {
|
387
|
-
controller.abort()
|
388
|
-
clearInterval(interval)
|
389
|
-
}
|
390
|
-
}
|
391
|
-
return undefined
|
392
|
-
}, [overlaysConnection, timedOut])
|
393
|
-
|
394
275
|
return (
|
395
276
|
<MotionConfig transition={prefersReducedMotion ? {duration: 0} : undefined}>
|
396
277
|
<TooltipDelayGroupProvider>
|
397
278
|
{previewHeader}
|
279
|
+
|
280
|
+
{/* @TODO: Move this to <PreviewFrame /> */}
|
398
281
|
<Card flex={1} tone="transparent">
|
399
282
|
<Flex
|
400
283
|
align="center"
|
@@ -409,8 +292,8 @@ export const Preview = memo(
|
|
409
292
|
>
|
410
293
|
<AnimatePresence>
|
411
294
|
{!somethingIsWrong &&
|
412
|
-
!
|
413
|
-
!
|
295
|
+
!loading &&
|
296
|
+
!refreshing &&
|
414
297
|
// viewport, // using CSS View Transitions instead of framer motion to drive this
|
415
298
|
showOverlaysConnectionStatus &&
|
416
299
|
!continueAnyway ? (
|
@@ -476,7 +359,8 @@ export const Preview = memo(
|
|
476
359
|
)}
|
477
360
|
</Flex>
|
478
361
|
</MotionFlex>
|
479
|
-
) : (
|
362
|
+
) : (loading ||
|
363
|
+
(overlaysConnection === 'connecting' && iframe.status !== 'refreshing')) &&
|
480
364
|
!continueAnyway ? (
|
481
365
|
<MotionFlex
|
482
366
|
initial="initial"
|