feat: improved live preview support
This commit is contained in:
parent
5046dc1558
commit
f696d5c445
@ -21,7 +21,7 @@ import {
|
||||
import { loadMedia } from '@staticcms/core/actions/mediaLibrary';
|
||||
import { loadScroll, toggleScroll } from '@staticcms/core/actions/scroll';
|
||||
import useEntryCallback from '@staticcms/core/lib/hooks/useEntryCallback';
|
||||
import { selectFields } from '@staticcms/core/lib/util/collection.util';
|
||||
import { getFileFromSlug, selectFields } from '@staticcms/core/lib/util/collection.util';
|
||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectEntry } from '@staticcms/core/reducers/selectors/entries';
|
||||
@ -211,7 +211,24 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
};
|
||||
}, [collection, createBackup, entryDraft.entry, hasChanged]);
|
||||
|
||||
const hasLivePreview = useMemo(() => {
|
||||
let livePreview = typeof collection.editor?.live_preview === 'string';
|
||||
|
||||
if ('files' in collection) {
|
||||
if (entryDraft.entry) {
|
||||
const file = getFileFromSlug(collection, entryDraft.entry.slug);
|
||||
|
||||
if (file?.editor) {
|
||||
livePreview = livePreview || typeof file.editor.live_preview === 'string';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return livePreview;
|
||||
}, [collection, entryDraft.entry]);
|
||||
|
||||
useEntryCallback({
|
||||
hasLivePreview,
|
||||
collection,
|
||||
slug,
|
||||
callback: () => {
|
||||
|
@ -8,6 +8,7 @@ import { EDITOR_SIZE_COMPACT } from '@staticcms/core/constants/views';
|
||||
import { getPreviewStyles, getPreviewTemplate } from '@staticcms/core/lib/registry';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { selectTemplateName } from '@staticcms/core/lib/util/collection.util';
|
||||
import LivePreviewLoadedEvent from '@staticcms/core/lib/util/events/LivePreviewLoadedEvent';
|
||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
@ -195,6 +196,10 @@ const EditorPreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
||||
|
||||
useWindowEvent('data:update', passEventToIframe);
|
||||
|
||||
const handleLivePreviewIframeLoaded = useCallback(() => {
|
||||
window.dispatchEvent(new LivePreviewLoadedEvent());
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!element) {
|
||||
return null;
|
||||
@ -222,9 +227,11 @@ const EditorPreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
||||
<ErrorBoundary config={config}>
|
||||
{livePreviewUrlTemplate ? (
|
||||
<iframe
|
||||
key="live-preview-frame"
|
||||
ref={livePreviewIframe}
|
||||
src={`${livePreviewUrlTemplate}?useCmsData=true`}
|
||||
className="w-full h-full"
|
||||
onLoad={handleLivePreviewIframeLoaded}
|
||||
/>
|
||||
) : previewInFrame ? (
|
||||
<Frame
|
||||
@ -280,6 +287,7 @@ const EditorPreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
||||
config,
|
||||
editorSize,
|
||||
element,
|
||||
handleLivePreviewIframeLoaded,
|
||||
initialFrameContent,
|
||||
livePreviewUrlTemplate,
|
||||
previewComponent,
|
||||
|
@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { currentBackend } from './backend';
|
||||
import bootstrap from './bootstrap';
|
||||
import useData from './lib/hooks/useData';
|
||||
import useEntries from './lib/hooks/useEntries';
|
||||
import useFolderSupport from './lib/hooks/useFolderSupport';
|
||||
import useHasChildErrors from './lib/hooks/useHasChildErrors';
|
||||
@ -13,6 +12,7 @@ import useMediaFiles from './lib/hooks/useMediaFiles';
|
||||
import useMediaInsert from './lib/hooks/useMediaInsert';
|
||||
import useUUID from './lib/hooks/useUUID';
|
||||
import Registry from './lib/registry';
|
||||
import useData from './live/useData';
|
||||
|
||||
export * from './backends';
|
||||
export * from './interface';
|
||||
|
@ -1,4 +1,4 @@
|
||||
export { default as useData } from './useData';
|
||||
export { default as useData } from '../../live/useData';
|
||||
export { default as useEntries } from './useEntries';
|
||||
export { default as useFolderSupport } from './useFolderSupport';
|
||||
export { default as useHasChildErrors } from './useHasChildErrors';
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { ValueOrNestedValue } from '@staticcms/core/interface';
|
||||
|
||||
export default function useData(value: ValueOrNestedValue, path: string) {
|
||||
const [data, setData] = useState(value);
|
||||
|
||||
const isCms = useMemo(() => {
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
return searchParams.get('useCmsData') === 'true';
|
||||
}, []);
|
||||
|
||||
const onDataChange = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
if (!isCms || event.data.message !== 'data:update') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fieldPath, value } = event.data.value;
|
||||
|
||||
if (fieldPath === path) {
|
||||
setData(value);
|
||||
}
|
||||
},
|
||||
[isCms, path],
|
||||
) as EventListenerOrEventListenerObject;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCms) {
|
||||
return;
|
||||
}
|
||||
|
||||
window?.addEventListener('message', onDataChange);
|
||||
|
||||
return () => {
|
||||
window?.removeEventListener('message', onDataChange);
|
||||
};
|
||||
}, [isCms, onDataChange]);
|
||||
|
||||
return data ?? null;
|
||||
}
|
@ -8,8 +8,9 @@ import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraf
|
||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import { invokeEvent } from '../registry';
|
||||
import { fileForEntry } from '../util/collection.util';
|
||||
import useDebouncedCallback from './useDebouncedCallback';
|
||||
import DataUpdateEvent from '../util/events/DataEvent';
|
||||
import { useWindowEvent } from '../util/window.util';
|
||||
import useDebouncedCallback from './useDebouncedCallback';
|
||||
|
||||
import type { Collection, EntryData, Field } from '@staticcms/core/interface';
|
||||
|
||||
@ -41,7 +42,7 @@ async function handleChange(
|
||||
window.dispatchEvent(
|
||||
new DataUpdateEvent({
|
||||
field: field.name,
|
||||
fieldPath,
|
||||
fieldPath: `${collection}.${fieldPath}`,
|
||||
value: updatedValue,
|
||||
}),
|
||||
);
|
||||
@ -63,19 +64,27 @@ async function handleChange(
|
||||
}
|
||||
|
||||
interface EntryCallbackProps {
|
||||
hasLivePreview: boolean;
|
||||
collection: Collection;
|
||||
slug: string | undefined;
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
export default function useEntryCallback({ slug, collection, callback }: EntryCallbackProps) {
|
||||
export default function useEntryCallback({
|
||||
hasLivePreview,
|
||||
slug,
|
||||
collection,
|
||||
callback,
|
||||
}: EntryCallbackProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [livePreviewLoaded, setLivePreviewLoaded] = useState(false);
|
||||
|
||||
const entry = useAppSelector(selectEditingDraft);
|
||||
const [lastEntryData, setLastEntryData] = useState<EntryData>(cloneDeep(entry?.data));
|
||||
|
||||
const runUpdateCheck = useCallback(async () => {
|
||||
if (isEqual(lastEntryData, entry?.data)) {
|
||||
if ((hasLivePreview && !livePreviewLoaded) || isEqual(lastEntryData, entry?.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -115,11 +124,17 @@ export default function useEntryCallback({ slug, collection, callback }: EntryCa
|
||||
setLastEntryData(entry?.data);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entry]);
|
||||
}, [entry, livePreviewLoaded]);
|
||||
|
||||
const debouncedRunUpdateCheck = useDebouncedCallback(runUpdateCheck, 200);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedRunUpdateCheck();
|
||||
}, [debouncedRunUpdateCheck]);
|
||||
|
||||
const handleLivePreviewLoaded = useCallback(() => {
|
||||
setLivePreviewLoaded(true);
|
||||
}, []);
|
||||
|
||||
useWindowEvent('livePreviewLoaded', handleLivePreviewLoaded);
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
export default class LivePreviewLoadedEvent extends CustomEvent<{}> {
|
||||
constructor() {
|
||||
super('livePreviewLoaded', {});
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||
import type AlertEvent from './events/AlertEvent';
|
||||
import type ConfirmEvent from './events/ConfirmEvent';
|
||||
import type DataUpdateEvent from './events/DataEvent';
|
||||
import type LivePreviewLoadedEvent from './events/LivePreviewLoadedEvent';
|
||||
import type MediaLibraryCloseEvent from './events/MediaLibraryCloseEvent';
|
||||
|
||||
interface EventMap {
|
||||
@ -10,6 +11,7 @@ interface EventMap {
|
||||
confirm: ConfirmEvent;
|
||||
mediaLibraryClose: MediaLibraryCloseEvent;
|
||||
'data:update': DataUpdateEvent;
|
||||
livePreviewLoaded: LivePreviewLoadedEvent;
|
||||
}
|
||||
|
||||
export function useWindowEvent<K extends keyof WindowEventMap>(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import useData from '@staticcms/core/lib/hooks/useData';
|
||||
import useData from './useData';
|
||||
|
||||
import type { ValueOrNestedValue } from '@staticcms/core/interface';
|
||||
import type { FC } from 'react';
|
||||
|
@ -1 +1,2 @@
|
||||
export { default as Data, type DataProps } from './Data';
|
||||
export { default as useData } from './useData';
|
||||
|
48
packages/core/src/live/useData.tsx
Normal file
48
packages/core/src/live/useData.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { ValueOrNestedValue } from '@staticcms/core/interface';
|
||||
|
||||
export default function useData<I extends ValueOrNestedValue, O extends ValueOrNestedValue>(
|
||||
value: O,
|
||||
path?: string,
|
||||
preprocessor?: (value: I) => O | Promise<O>,
|
||||
) {
|
||||
const [data, setData] = useState(value);
|
||||
|
||||
const isCms = useMemo(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
return searchParams.get('useCmsData') === 'true';
|
||||
}, []);
|
||||
|
||||
const onDataChange = useCallback(
|
||||
async (event: MessageEvent) => {
|
||||
if (!isCms || !path || event.data.message !== 'data:update') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fieldPath, value } = event.data.value;
|
||||
|
||||
if (fieldPath === path) {
|
||||
setData(preprocessor ? await preprocessor(value) : value);
|
||||
}
|
||||
},
|
||||
[isCms, path, preprocessor],
|
||||
) as unknown as EventListenerOrEventListenerObject;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCms || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('message', onDataChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', onDataChange);
|
||||
};
|
||||
}, [isCms, onDataChange]);
|
||||
|
||||
return data ?? value;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user