From 09e349ad451f6667368dec0a3a23c11e4e1eb63d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?=
<8087692+remidej@users.noreply.github.com>
Date: Tue, 19 Aug 2025 11:25:07 -0400
Subject: [PATCH] future: sync preview to side editor forms (#24179)
* feat: live preview via iframe events
* fix: only show focus style if not in popover
* chore: extract Blocker component
* chore: single WILL_EDIT_FIELD listener
* chore: rename constants
* fix: hovering focused highlight
* fix: remove useless highlights
* fix: adrien feedback
---
.../admin/src/pages/EditView/EditViewPage.tsx | 14 +-
.../src/pages/EditView/components/Blocker.tsx | 15 ++
.../EditView/components/InputRenderer.tsx | 38 ++-
.../src/preview/components/InputPopover.tsx | 85 ++++++
.../preview/hooks/usePreviewInputManager.ts | 49 ++++
.../admin/src/preview/pages/Preview.tsx | 247 +++++++++---------
.../admin/src/preview/utils/getSendMessage.ts | 27 ++
.../admin/src/preview/utils/previewScript.ts | 130 ++++++++-
8 files changed, 451 insertions(+), 154 deletions(-)
create mode 100644 packages/core/content-manager/admin/src/pages/EditView/components/Blocker.tsx
create mode 100644 packages/core/content-manager/admin/src/preview/components/InputPopover.tsx
create mode 100644 packages/core/content-manager/admin/src/preview/hooks/usePreviewInputManager.ts
create mode 100644 packages/core/content-manager/admin/src/preview/utils/getSendMessage.ts
diff --git a/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx b/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx
index e662d53cb8..b86d712c96 100644
--- a/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx
+++ b/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx
@@ -2,9 +2,7 @@ import * as React from 'react';
import {
Page,
- Blocker,
Form,
- useForm,
useRBAC,
useNotification,
useQueryParams,
@@ -25,6 +23,7 @@ import { useOnce } from '../../hooks/useOnce';
import { getTranslation } from '../../utils/translations';
import { createYupSchema } from '../../utils/validation';
+import { Blocker } from './components/Blocker';
import { FormLayout } from './components/FormLayout';
import { Header } from './components/Header';
import { Panels } from './components/Panels';
@@ -34,15 +33,6 @@ import { handleInvisibleAttributes } from './utils/data';
* EditViewPage
* -----------------------------------------------------------------------------------------------*/
-// Needs to be wrapped in a component to have access to the form context via a hook.
-// Using the Form component's render prop instead would cause unnecessary re-renders of Form children
-const BlockerWrapper = () => {
- const resetForm = useForm('BlockerWrapper', (state) => state.resetForm);
-
- // We reset the form to the published version to avoid errors like – https://strapi-inc.atlassian.net/browse/CONTENT-2284
- return ;
-};
-
const EditViewPage = () => {
const location = useLocation();
const [
@@ -220,7 +210,7 @@ const EditViewPage = () => {
-
+
>
diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/Blocker.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/Blocker.tsx
new file mode 100644
index 0000000000..a240825dcf
--- /dev/null
+++ b/packages/core/content-manager/admin/src/pages/EditView/components/Blocker.tsx
@@ -0,0 +1,15 @@
+// Needs to be wrapped in a component to have access to the form context via a hook.
+
+import { Blocker as BaseBlocker, useForm } from '@strapi/admin/strapi-admin';
+
+/**
+ * Prevents users from leaving the page with unsaved form changes
+ */
+const Blocker = () => {
+ const resetForm = useForm('Blocker', (state) => state.resetForm);
+
+ // We reset the form to the published version to avoid errors like – https://strapi-inc.atlassian.net/browse/CONTENT-2284
+ return ;
+};
+
+export { Blocker };
diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx
index 2a5e455e62..08fc9a9ff3 100644
--- a/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx
+++ b/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx
@@ -14,6 +14,7 @@ import { type UseDocument } from '../../../hooks/useDocument';
import { useDocumentContext } from '../../../hooks/useDocumentContext';
import { useDocumentLayout } from '../../../hooks/useDocumentLayout';
import { useLazyComponents } from '../../../hooks/useLazyComponents';
+import { usePreviewInputManager } from '../../../preview/hooks/usePreviewInputManager';
import { BlocksInput } from './FormInputs/BlocksInput/BlocksInput';
import { ComponentInput } from './FormInputs/Component/Input';
@@ -39,7 +40,12 @@ type InputRendererProps = DistributiveOmit & {
* the complete EditFieldLayout and will handle RBAC conditions and rendering CM specific
* components such as Blocks / Relations.
*/
-const InputRenderer = ({ visible, hint: providedHint, document, ...props }: InputRendererProps) => {
+const InputRenderer = ({
+ visible,
+ hint: providedHint,
+ document,
+ ...inputProps
+}: InputRendererProps) => {
const { currentDocumentMeta } = useDocumentContext('DynamicComponent');
const {
edit: { components },
@@ -64,6 +70,10 @@ const InputRenderer = ({ visible, hint: providedHint, document, ...props }: Inpu
const editableFields = idToCheck ? canUpdateFields : canCreateFields;
const readableFields = idToCheck ? canReadFields : canCreateFields;
+ // Everything preview related
+ const previewProps = usePreviewInputManager(inputProps.name);
+ const props = { ...inputProps, ...previewProps };
+
/**
* Component fields are always readable and editable,
* however the fields within them may not be.
@@ -103,13 +113,21 @@ const InputRenderer = ({ visible, hint: providedHint, document, ...props }: Inpu
const CustomInput = lazyComponentStore[props.attribute.customField];
if (CustomInput) {
- // @ts-expect-error – TODO: fix this type error in the useLazyComponents hook.
- return ;
+ return (
+
+ );
}
return (
;
+ return (
+
+ );
}
/**
@@ -143,7 +167,7 @@ const InputRenderer = ({ visible, hint: providedHint, document, ...props }: Inpu
layout={components[props.attribute.component].layout}
disabled={fieldIsDisabled}
>
- {(inputProps) => }
+ {(componentInputProps) => }
);
case 'dynamiczone':
@@ -161,6 +185,7 @@ const InputRenderer = ({ visible, hint: providedHint, document, ...props }: Inpu
return (
({ value }))}
// @ts-expect-error – Temp workaround so we don't forget custom-fields don't work!
@@ -174,6 +199,7 @@ const InputRenderer = ({ visible, hint: providedHint, document, ...props }: Inpu
return (
('InputPopover');
+
+const InputPopover = ({ documentResponse }: { documentResponse: ReturnType }) => {
+ const iframeRef = usePreviewContext('VisualEditingPopover', (state) => state.iframeRef);
+ const popoverField = usePreviewContext('VisualEditingPopover', (state) => state.popoverField);
+ const setPopoverField = usePreviewContext(
+ 'VisualEditingPopover',
+ (state) => state.setPopoverField
+ );
+
+ if (!popoverField || !documentResponse.schema || !iframeRef.current) {
+ return null;
+ }
+
+ const iframeRect = iframeRef.current.getBoundingClientRect();
+
+ return (
+ <>
+ {/**
+ * Overlay an empty div on top of the iframe while the popover is open so it can
+ * intercept clicks. Without it, we wouldn't be able to close the popover by clicking outside,
+ * because the click would be detected by the iframe window, not by the admin.
+ **/}
+
+
+ !open && setPopoverField(null)}>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+function useHasInputPopoverParent() {
+ const context = useInputPopoverContext('useHasInputPopoverParent', () => true, false);
+
+ // useContext will return undefined if the called is not wrapped in the provider
+ return context !== undefined;
+}
+
+export { InputPopover, useHasInputPopoverParent };
diff --git a/packages/core/content-manager/admin/src/preview/hooks/usePreviewInputManager.ts b/packages/core/content-manager/admin/src/preview/hooks/usePreviewInputManager.ts
new file mode 100644
index 0000000000..215ff11cdc
--- /dev/null
+++ b/packages/core/content-manager/admin/src/preview/hooks/usePreviewInputManager.ts
@@ -0,0 +1,49 @@
+import * as React from 'react';
+
+import { useField } from '@strapi/admin/strapi-admin';
+
+import { useHasInputPopoverParent } from '../components/InputPopover';
+import { usePreviewContext } from '../pages/Preview';
+import { INTERNAL_EVENTS } from '../utils/constants';
+import { getSendMessage } from '../utils/getSendMessage';
+
+type PreviewInputProps = Pick<
+ Required>,
+ 'onFocus' | 'onBlur'
+>;
+
+export function usePreviewInputManager(name: string): PreviewInputProps {
+ const iframe = usePreviewContext('usePreviewInputManager', (state) => state.iframeRef, false);
+ const setPopoverField = usePreviewContext(
+ 'usePreviewInputManager',
+ (state) => state.setPopoverField,
+ false
+ );
+ const hasInputPopoverParent = useHasInputPopoverParent();
+ const { value } = useField(name);
+
+ React.useEffect(() => {
+ if (!iframe) {
+ return;
+ }
+
+ const sendMessage = getSendMessage(iframe);
+ sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_CHANGE, { field: name, value });
+ }, [name, value, iframe]);
+
+ const sendMessage = getSendMessage(iframe);
+
+ return {
+ onFocus: () => {
+ if (hasInputPopoverParent) return;
+
+ sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_FOCUS, { field: name });
+ },
+ onBlur: () => {
+ if (hasInputPopoverParent) return;
+
+ setPopoverField?.(null);
+ sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_BLUR, { field: name });
+ },
+ };
+}
diff --git a/packages/core/content-manager/admin/src/preview/pages/Preview.tsx b/packages/core/content-manager/admin/src/preview/pages/Preview.tsx
index 1fea305c86..e5df1e7fe6 100644
--- a/packages/core/content-manager/admin/src/preview/pages/Preview.tsx
+++ b/packages/core/content-manager/admin/src/preview/pages/Preview.tsx
@@ -6,7 +6,6 @@ import {
useRBAC,
createContext,
Form as FormContext,
- Blocker,
} from '@strapi/admin/strapi-admin';
import {
Box,
@@ -27,13 +26,16 @@ import { COLLECTION_TYPES } from '../../constants/collections';
import { DocumentRBAC } from '../../features/DocumentRBAC';
import { type UseDocument, useDocument } from '../../hooks/useDocument';
import { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout';
+import { Blocker } from '../../pages/EditView/components/Blocker';
import { FormLayout } from '../../pages/EditView/components/FormLayout';
import { handleInvisibleAttributes } from '../../pages/EditView/utils/data';
import { buildValidParams } from '../../utils/api';
import { createYupSchema } from '../../utils/validation';
+import { InputPopover } from '../components/InputPopover';
import { PreviewHeader } from '../components/PreviewHeader';
import { useGetPreviewUrlQuery } from '../services/preview';
-import { INTERNAL_EVENTS, PUBLIC_EVENTS } from '../utils/constants';
+import { PUBLIC_EVENTS, INTERNAL_EVENTS } from '../utils/constants';
+import { getSendMessage } from '../utils/getSendMessage';
import { previewScript } from '../utils/previewScript';
import type { UID } from '@strapi/types';
@@ -67,6 +69,11 @@ const DEVICES = [
* PreviewProvider
* -----------------------------------------------------------------------------------------------*/
+interface PopoverField {
+ path: string;
+ position: DOMRect;
+}
+
interface PreviewContextValue {
url: string;
title: string;
@@ -75,6 +82,9 @@ interface PreviewContextValue {
schema: NonNullable['schema']>;
layout: EditLayout;
onPreview: () => void;
+ iframeRef: React.RefObject;
+ popoverField: PopoverField | null;
+ setPopoverField: (value: PopoverField | null) => void;
}
const [PreviewProvider, usePreviewContext] = createContext('PreviewPage');
@@ -89,38 +99,13 @@ const AnimatedArrow = styled(ArrowLineLeft)<{ $isSideEditorOpen: boolean }>`
transition: rotate 0.2s ease-in-out;
`;
-type MessageType =
- | (typeof INTERNAL_EVENTS)[keyof typeof INTERNAL_EVENTS]
- | (typeof PUBLIC_EVENTS)[keyof typeof PUBLIC_EVENTS];
-
-/**
- * A function factory so we can generate a new sendMessage everytime we need one.
- * We can't store and reuse a single sendMessage because it needs to have a stable identity
- * as it used in a useEffect function. And we can't rely on useCallback because we need the
- * up-to-date iframe ref, and this would make it stale (refs don't trigger callback reevaluations).
- */
-function getSendMessage(iframe: React.RefObject) {
- return (type: MessageType, payload?: unknown) => {
- if (!iframe.current) return;
-
- const { origin } = new URL(iframe.current.src);
-
- iframe.current.contentWindow?.postMessage(
- {
- type,
- ...(payload !== undefined && { payload }),
- },
- origin
- );
- };
-}
-
const PreviewPage = () => {
const location = useLocation();
const { formatMessage } = useIntl();
const iframeRef = React.useRef(null);
const [isSideEditorOpen, setIsSideEditorOpen] = React.useState(true);
+ const [popoverField, setPopoverField] = React.useState(null);
// Read all the necessary data from the URL to find the right preview URL
const {
@@ -147,11 +132,23 @@ const PreviewPage = () => {
// Listen for ready message from iframe before injecting script
React.useEffect(() => {
const handleMessage = (event: MessageEvent) => {
+ // Only listen to events from the preview iframe
+ if (iframeRef.current) {
+ const previewOrigin = new URL(iframeRef.current?.src).origin;
+ if (event.origin !== previewOrigin) {
+ return;
+ }
+ }
+
if (event.data?.type === PUBLIC_EVENTS.PREVIEW_READY) {
const script = `(${previewScript.toString()})()`;
const sendMessage = getSendMessage(iframeRef);
sendMessage(PUBLIC_EVENTS.STRAPI_SCRIPT, { script });
}
+
+ if (event.data?.type === INTERNAL_EVENTS.STRAPI_FIELD_FOCUS_INTENT) {
+ setPopoverField?.(event.data.payload);
+ }
};
window.addEventListener('message', handleMessage);
@@ -270,6 +267,9 @@ const PreviewPage = () => {
schema={documentResponse.schema}
layout={documentLayoutResponse.edit}
onPreview={onPreview}
+ iframeRef={iframeRef}
+ popoverField={popoverField}
+ setPopoverField={setPopoverField}
>
{
return yupSchema.validate(cleanedValues, { abortEarly: false });
}}
>
- {({ resetForm }) => (
-
-
-
-
- {hasAdvancedPreview && (
-
-
-
- )}
-
+
+
+
+
+ {hasAdvancedPreview && (
+
-
- {hasAdvancedPreview && (
- setIsSideEditorOpen((prev) => !prev)}
- >
-
-
- )}
-
- setDeviceName(name.toString())}
- aria-label={formatMessage({
- id: 'content-manager.preview.device.select',
- defaultMessage: 'Select device type',
- })}
- >
- {DEVICES.map((deviceOption) => (
-
- {formatMessage(deviceOption.label)}
-
- ))}
-
-
-
-
-
+
+ )}
+
+
+ {hasAdvancedPreview && (
+ setIsSideEditorOpen((prev) => !prev)}
+ >
+
+
+ )}
+
+ setDeviceName(name.toString())}
+ aria-label={formatMessage({
+ id: 'content-manager.preview.device.select',
+ defaultMessage: 'Select device type',
})}
- width={device.width}
- height={device.height}
- borderWidth={0}
- tag="iframe"
- />
+ >
+ {DEVICES.map((deviceOption) => (
+
+ {formatMessage(deviceOption.label)}
+
+ ))}
+
+
+
+
- )}
+
>
diff --git a/packages/core/content-manager/admin/src/preview/utils/getSendMessage.ts b/packages/core/content-manager/admin/src/preview/utils/getSendMessage.ts
new file mode 100644
index 0000000000..eacc3cae4a
--- /dev/null
+++ b/packages/core/content-manager/admin/src/preview/utils/getSendMessage.ts
@@ -0,0 +1,27 @@
+import type { INTERNAL_EVENTS, PUBLIC_EVENTS } from './constants';
+
+type MessageType =
+ | (typeof INTERNAL_EVENTS)[keyof typeof INTERNAL_EVENTS]
+ | (typeof PUBLIC_EVENTS)[keyof typeof PUBLIC_EVENTS];
+
+/**
+ * A function factory so we can generate a new sendMessage everytime we need one.
+ * We can't store and reuse a single sendMessage because it needs to have a stable identity
+ * as it used in a useEffect function. And we can't rely on useCallback because we need the
+ * up-to-date iframe ref, and this would make it stale (refs don't trigger callback reevaluations).
+ */
+export function getSendMessage(iframe: React.RefObject | undefined) {
+ return (type: MessageType, payload?: unknown) => {
+ if (!iframe?.current) return;
+
+ const { origin } = new URL(iframe.current.src);
+
+ iframe.current.contentWindow?.postMessage(
+ {
+ type,
+ ...(payload !== undefined && { payload }),
+ },
+ origin
+ );
+ };
+}
diff --git a/packages/core/content-manager/admin/src/preview/utils/previewScript.ts b/packages/core/content-manager/admin/src/preview/utils/previewScript.ts
index cbde454392..9240f2843a 100644
--- a/packages/core/content-manager/admin/src/preview/utils/previewScript.ts
+++ b/packages/core/content-manager/admin/src/preview/utils/previewScript.ts
@@ -3,6 +3,7 @@ declare global {
interface Window {
__strapi_previewCleanup?: () => void;
STRAPI_HIGHLIGHT_HOVER_COLOR?: string;
+ STRAPI_HIGHLIGHT_ACTIVE_COLOR?: string;
}
}
@@ -18,10 +19,15 @@ const previewScript = (shouldRun = true) => {
* ---------------------------------------------------------------------------------------------*/
const HIGHLIGHT_PADDING = 2; // in pixels
const HIGHLIGHT_HOVER_COLOR = window.STRAPI_HIGHLIGHT_HOVER_COLOR ?? '#4945ff'; // dark primary500
+ const HIGHLIGHT_ACTIVE_COLOR = window.STRAPI_HIGHLIGHT_ACTIVE_COLOR ?? '#7b79ff'; // dark primary600
+
const SOURCE_ATTRIBUTE = 'data-strapi-source';
const OVERLAY_ID = 'strapi-preview-overlay';
const INTERNAL_EVENTS = {
- DUMMY_EVENT: 'dummyEvent',
+ STRAPI_FIELD_FOCUS: 'strapiFieldFocus',
+ STRAPI_FIELD_BLUR: 'strapiFieldBlur',
+ STRAPI_FIELD_CHANGE: 'strapiFieldChange',
+ STRAPI_FIELD_FOCUS_INTENT: 'strapiFieldFocusIntent',
} as const;
/**
@@ -33,6 +39,17 @@ const previewScript = (shouldRun = true) => {
return { INTERNAL_EVENTS };
}
+ /* -----------------------------------------------------------------------------------------------
+ * Utils
+ * ---------------------------------------------------------------------------------------------*/
+
+ const sendMessage = (
+ type: (typeof INTERNAL_EVENTS)[keyof typeof INTERNAL_EVENTS],
+ payload: unknown
+ ) => {
+ window.parent.postMessage({ type, payload }, '*');
+ };
+
/* -----------------------------------------------------------------------------------------------
* Functionality pieces
* ---------------------------------------------------------------------------------------------*/
@@ -59,15 +76,17 @@ const previewScript = (shouldRun = true) => {
};
type EventListenersList = Array<{
- element: HTMLElement;
- type: keyof HTMLElementEventMap;
+ element: HTMLElement | Window;
+ type: keyof HTMLElementEventMap | 'message';
handler: EventListener;
}>;
const createHighlightManager = (overlay: HTMLElement) => {
const elements = window.document.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`);
- const highlights: HTMLElement[] = [];
const eventListeners: EventListenersList = [];
+ const highlights: HTMLElement[] = [];
+ const focusedHighlights: HTMLElement[] = [];
+ let focusedField: string | null = null;
const drawHighlight = (target: Element, highlight: HTMLElement) => {
if (!highlight) return;
@@ -102,15 +121,31 @@ const previewScript = (shouldRun = true) => {
// Move hover detection to the underlying element
const mouseEnterHandler = () => {
- highlight.style.outlineColor = HIGHLIGHT_HOVER_COLOR;
+ if (!highlightManager.focusedHighlights.includes(highlight)) {
+ highlight.style.outlineColor = HIGHLIGHT_HOVER_COLOR;
+ }
};
const mouseLeaveHandler = () => {
- highlight.style.outlineColor = 'transparent';
+ if (!highlightManager.focusedHighlights.includes(highlight)) {
+ highlight.style.outlineColor = 'transparent';
+ }
};
const doubleClickHandler = () => {
- // TODO: handle for real
- // eslint-disable-next-line no-console
- console.log('Double click on highlight', element);
+ const sourceAttribute = element.getAttribute(SOURCE_ATTRIBUTE);
+ if (sourceAttribute) {
+ const rect = element.getBoundingClientRect();
+ sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_FOCUS_INTENT, {
+ path: sourceAttribute,
+ position: {
+ top: rect.top,
+ left: rect.left,
+ right: rect.right,
+ bottom: rect.bottom,
+ width: rect.width,
+ height: rect.height,
+ },
+ });
+ }
};
const mouseDownHandler = (event: MouseEvent) => {
// Prevent default multi click to select behavior
@@ -143,6 +178,12 @@ const previewScript = (shouldRun = true) => {
elements,
updateAllHighlights,
eventListeners,
+ highlights,
+ focusedHighlights,
+ setFocusedField: (field: string | null) => {
+ focusedField = field;
+ },
+ getFocusedField: () => focusedField,
};
};
@@ -187,7 +228,7 @@ const previewScript = (shouldRun = true) => {
window.addEventListener('scroll', updateOnScroll);
window.addEventListener('resize', updateOnScroll);
} else {
- (element as Element).addEventListener('scroll', updateOnScroll);
+ element.addEventListener('scroll', updateOnScroll);
}
});
@@ -199,8 +240,73 @@ const previewScript = (shouldRun = true) => {
};
const setupEventHandlers = (highlightManager: HighlightManager) => {
- // TODO: The listeners for postMessage events will go here
- return highlightManager.eventListeners;
+ const handleMessage = (event: MessageEvent) => {
+ if (!event.data?.type) return;
+
+ // The user typed in an input, reflect the change in the preview
+ if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_CHANGE) {
+ const { field, value } = event.data.payload;
+ if (!field) return;
+
+ const matchingElements = document.querySelectorAll(`[${SOURCE_ATTRIBUTE}="${field}"]`);
+ matchingElements.forEach((element) => {
+ if (element instanceof HTMLElement) {
+ element.textContent = value || '';
+ }
+ });
+ return;
+ }
+
+ // The user focused a new input, update the highlights in the preview
+ if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_FOCUS) {
+ const { field } = event.data.payload;
+ if (!field) return;
+
+ // Clear existing focused highlights
+ highlightManager.focusedHighlights.forEach((highlight: HTMLElement) => {
+ highlight.style.outlineColor = 'transparent';
+ });
+ highlightManager.focusedHighlights.length = 0;
+
+ // Set new focused field and highlight matching elements
+ highlightManager.setFocusedField(field);
+ const matchingElements = document.querySelectorAll(`[${SOURCE_ATTRIBUTE}="${field}"]`);
+ matchingElements.forEach((element) => {
+ const highlight =
+ highlightManager.highlights[Array.from(highlightManager.elements).indexOf(element)];
+ if (highlight) {
+ highlight.style.outlineColor = HIGHLIGHT_ACTIVE_COLOR;
+ highlight.style.outlineWidth = '3px';
+ highlightManager.focusedHighlights.push(highlight);
+ }
+ });
+ return;
+ }
+
+ // The user is no longer focusing an input, remove the highlights
+ if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_BLUR) {
+ const { field } = event.data.payload;
+ if (field !== highlightManager.getFocusedField()) return;
+
+ highlightManager.focusedHighlights.forEach((highlight: HTMLElement) => {
+ highlight.style.outlineColor = 'transparent';
+ highlight.style.outlineWidth = '2px';
+ });
+ highlightManager.focusedHighlights.length = 0;
+ highlightManager.setFocusedField(null);
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+
+ // Add the message handler to the cleanup list
+ const messageEventListener = {
+ element: window,
+ type: 'message' as keyof HTMLElementEventMap,
+ handler: handleMessage as EventListener,
+ };
+
+ return [...highlightManager.eventListeners, messageEventListener];
};
const createCleanupSystem = (