@parhelia/core 0.1.12879 → 0.1.12880
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/dist/components/ui/card.d.ts +1 -3
- package/dist/components/ui/card.js +2 -2
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/context-menu.js +2 -2
- package/dist/config/config.js +6 -23
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +7 -0
- package/dist/config/types.js.map +1 -1
- package/dist/editor/FieldActionsOverlay.d.ts +1 -0
- package/dist/editor/FieldActionsOverlay.js +45 -1
- package/dist/editor/FieldActionsOverlay.js.map +1 -1
- package/dist/editor/FieldListField.d.ts +1 -1
- package/dist/editor/FieldListField.js +18 -20
- package/dist/editor/FieldListField.js.map +1 -1
- package/dist/editor/ImageEditor.d.ts +1 -6
- package/dist/editor/ImageEditor.js +3 -19
- package/dist/editor/ImageEditor.js.map +1 -1
- package/dist/editor/PictureEditor.d.ts +1 -2
- package/dist/editor/PictureEditor.js +14 -5
- package/dist/editor/PictureEditor.js.map +1 -1
- package/dist/editor/ai/Agents.js +2 -2
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/ai/GuidanceOverlay.js +11 -1
- package/dist/editor/ai/GuidanceOverlay.js.map +1 -1
- package/dist/editor/ai/InlineAiDialog.js +11 -22
- package/dist/editor/ai/InlineAiDialog.js.map +1 -1
- package/dist/editor/ai/InlineAiTrigger.js +57 -17
- package/dist/editor/ai/InlineAiTrigger.js.map +1 -1
- package/dist/editor/ai/dialogs/capturePageDom.js +36 -66
- package/dist/editor/ai/dialogs/capturePageDom.js.map +1 -1
- package/dist/editor/ai/dialogs/capturePageScreenshot.js +162 -281
- package/dist/editor/ai/dialogs/capturePageScreenshot.js.map +1 -1
- package/dist/editor/ai/terminal/agentSessionState.d.ts +0 -3
- package/dist/editor/ai/terminal/agentSessionState.js +1 -3
- package/dist/editor/ai/terminal/agentSessionState.js.map +1 -1
- package/dist/editor/ai/terminal/agentStartRequest.d.ts +1 -2
- package/dist/editor/ai/terminal/agentStartRequest.js +1 -2
- package/dist/editor/ai/terminal/agentStartRequest.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentCostDisplay.js +1 -1
- package/dist/editor/ai/terminal/components/AgentCostDisplay.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentDocumentList.d.ts +0 -7
- package/dist/editor/ai/terminal/components/AgentDocumentList.js +13 -55
- package/dist/editor/ai/terminal/components/AgentDocumentList.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentFullPromptControls.d.ts +1 -3
- package/dist/editor/ai/terminal/components/AgentFullPromptControls.js +14 -22
- package/dist/editor/ai/terminal/components/AgentFullPromptControls.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentModeSelector.js +4 -4
- package/dist/editor/ai/terminal/components/AgentModeSelector.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentPromptActionButtons.js +4 -4
- package/dist/editor/ai/terminal/components/AgentPromptActionButtons.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentPromptComposer.js +1 -1
- package/dist/editor/ai/terminal/components/AgentPromptComposer.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentPromptInputArea.d.ts +1 -2
- package/dist/editor/ai/terminal/components/AgentPromptInputArea.js +11 -8
- package/dist/editor/ai/terminal/components/AgentPromptInputArea.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.d.ts +4 -1
- package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.js +13 -29
- package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentSettingsPopover.js +1 -1
- package/dist/editor/ai/terminal/components/AgentSettingsPopover.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentTerminalFullLayout.d.ts +1 -2
- package/dist/editor/ai/terminal/components/AgentTerminalFullLayout.js +4 -2
- package/dist/editor/ai/terminal/components/AgentTerminalFullLayout.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentTerminalMessageGroups.js +1 -1
- package/dist/editor/ai/terminal/components/AgentTerminalMessageGroups.js.map +1 -1
- package/dist/editor/ai/terminal/components/AgentTerminalView.js +2 -13
- package/dist/editor/ai/terminal/components/AgentTerminalView.js.map +1 -1
- package/dist/editor/ai/terminal/components/AiResponseMessage.js +14 -16
- package/dist/editor/ai/terminal/components/AiResponseMessage.js.map +1 -1
- package/dist/editor/ai/terminal/components/QueuedPromptsPanel.js +26 -37
- package/dist/editor/ai/terminal/components/QueuedPromptsPanel.js.map +1 -1
- package/dist/editor/ai/terminal/components/UserMessage.d.ts +1 -2
- package/dist/editor/ai/terminal/components/UserMessage.js +8 -130
- package/dist/editor/ai/terminal/components/UserMessage.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentPromptComposerHandlers.js +1 -1
- package/dist/editor/ai/terminal/useAgentPromptComposerHandlers.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentSessionSync.d.ts +0 -1
- package/dist/editor/ai/terminal/useAgentSubmitHandlers.d.ts +1 -3
- package/dist/editor/ai/terminal/useAgentSubmitHandlers.js +3 -9
- package/dist/editor/ai/terminal/useAgentSubmitHandlers.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentTerminalController.js +0 -7
- package/dist/editor/ai/terminal/useAgentTerminalController.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentTerminalUiState.js +1 -1
- package/dist/editor/ai/terminal/useAgentTerminalUiState.js.map +1 -1
- package/dist/editor/ai/terminal/useAgentUserMessageSocketHandler.js +1 -3
- package/dist/editor/ai/terminal/useAgentUserMessageSocketHandler.js.map +1 -1
- package/dist/editor/ai/useInlineAiPosition.d.ts +1 -1
- package/dist/editor/ai/useInlineAiPosition.js +52 -22
- package/dist/editor/ai/useInlineAiPosition.js.map +1 -1
- package/dist/editor/ai-image-editor/AiImageResultOverlay.js +62 -30
- package/dist/editor/ai-image-editor/AiImageResultOverlay.js.map +1 -1
- package/dist/editor/client/EditorShell.d.ts +1 -5
- package/dist/editor/client/EditorShell.js +126 -286
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +5 -37
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/hooks/useSocketMessageHandler.js +17 -14
- package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
- package/dist/editor/client/itemsRepository.d.ts +0 -2
- package/dist/editor/client/itemsRepository.js +8 -15
- package/dist/editor/client/itemsRepository.js.map +1 -1
- package/dist/editor/client/operations.d.ts +1 -1
- package/dist/editor/client/operations.js +17 -41
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/client/pageModelBuilder.js +7 -24
- package/dist/editor/client/pageModelBuilder.js.map +1 -1
- package/dist/editor/field-types/MultiLineText.js +3 -10
- package/dist/editor/field-types/MultiLineText.js.map +1 -1
- package/dist/editor/field-types/RawEditor.js +1 -8
- package/dist/editor/field-types/RawEditor.js.map +1 -1
- package/dist/editor/field-types/RichTextEditorComponent.js +45 -156
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/SingleLineText.js +3 -10
- package/dist/editor/field-types/SingleLineText.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +2 -8
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/field-types/richtext/contextMenuFactory.d.ts +2 -1
- package/dist/editor/field-types/richtext/contextMenuFactory.js +303 -100
- package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
- package/dist/editor/field-types/richtext/types.d.ts +0 -2
- package/dist/editor/field-types/richtext/types.js.map +1 -1
- package/dist/editor/media-selector/TreeSelector.js +15 -15
- package/dist/editor/media-selector/TreeSelector.js.map +1 -1
- package/dist/editor/menubar/PageSelector.js +2 -8
- package/dist/editor/menubar/PageSelector.js.map +1 -1
- package/dist/editor/menubar/VersionPreviewCard.js +249 -4
- package/dist/editor/menubar/VersionPreviewCard.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/EditControls.js +2 -2
- package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/ManualBrowser.d.ts +10 -0
- package/dist/editor/menubar/toolbar-sections/ManualBrowser.js +462 -63
- package/dist/editor/menubar/toolbar-sections/ManualBrowser.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/ViewportControls.js +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlightings.d.ts +2 -5
- package/dist/editor/page-editor-chrome/CommentHighlightings.js +215 -340
- package/dist/editor/page-editor-chrome/CommentHighlightings.js.map +1 -1
- package/dist/editor/page-editor-chrome/FeedbackHighlightBadge.d.ts +1 -5
- package/dist/editor/page-editor-chrome/FeedbackHighlightBadge.js +4 -11
- package/dist/editor/page-editor-chrome/FeedbackHighlightBadge.js.map +1 -1
- package/dist/editor/page-editor-chrome/FieldActionIndicator.js +13 -21
- package/dist/editor/page-editor-chrome/FieldActionIndicator.js.map +1 -1
- package/dist/editor/page-editor-chrome/FieldEditedIndicator.js +29 -23
- package/dist/editor/page-editor-chrome/FieldEditedIndicator.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +19 -110
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-editor-chrome/InlineEditor.d.ts +7 -0
- package/dist/editor/page-editor-chrome/InlineEditor.js +1719 -0
- package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -0
- package/dist/editor/page-editor-chrome/LockedFieldIndicator.d.ts +2 -3
- package/dist/editor/page-editor-chrome/LockedFieldIndicator.js +45 -148
- package/dist/editor/page-editor-chrome/LockedFieldIndicator.js.map +1 -1
- package/dist/editor/page-editor-chrome/PageEditorChrome.d.ts +0 -2
- package/dist/editor/page-editor-chrome/PageEditorChrome.js +21 -25
- package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
- package/dist/editor/page-editor-chrome/PictureEditorOverlay.js +128 -163
- package/dist/editor/page-editor-chrome/PictureEditorOverlay.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +3 -6
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.d.ts +2 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +146 -83
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
- package/dist/editor/page-editor-chrome/SuggestionHighlightings.d.ts +2 -5
- package/dist/editor/page-editor-chrome/SuggestionHighlightings.js +63 -144
- package/dist/editor/page-editor-chrome/SuggestionHighlightings.js.map +1 -1
- package/dist/editor/page-editor-chrome/VersionDiffHighlightings.d.ts +2 -1
- package/dist/editor/page-editor-chrome/VersionDiffHighlightings.js +30 -101
- package/dist/editor/page-editor-chrome/VersionDiffHighlightings.js.map +1 -1
- package/dist/editor/page-editor-chrome/overlay/IframeOverlayProvider.d.ts +1 -10
- package/dist/editor/page-editor-chrome/overlay/IframeOverlayProvider.js +122 -105
- package/dist/editor/page-editor-chrome/overlay/IframeOverlayProvider.js.map +1 -1
- package/dist/editor/page-editor-chrome/overlay/geometry.d.ts +4 -11
- package/dist/editor/page-editor-chrome/overlay/geometry.js +36 -139
- package/dist/editor/page-editor-chrome/overlay/geometry.js.map +1 -1
- package/dist/editor/page-editor-chrome/overlay/iframeAccess.d.ts +2 -0
- package/dist/editor/page-editor-chrome/overlay/iframeAccess.js +21 -0
- package/dist/editor/page-editor-chrome/overlay/iframeAccess.js.map +1 -0
- package/dist/editor/page-editor-chrome/useInlineAICompletion.d.ts +7 -0
- package/dist/editor/page-editor-chrome/useInlineAICompletion.js +758 -0
- package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -0
- package/dist/editor/page-viewer/EditorForm.js +1 -17
- package/dist/editor/page-viewer/EditorForm.js.map +1 -1
- package/dist/editor/page-viewer/MiniMap.d.ts +2 -2
- package/dist/editor/page-viewer/MiniMap.js +364 -176
- package/dist/editor/page-viewer/MiniMap.js.map +1 -1
- package/dist/editor/page-viewer/PageViewer.js +13 -40
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.d.ts +5 -0
- package/dist/editor/page-viewer/PageViewerFrame.js +1513 -1529
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/page-viewer/pageModelSkeletonBuilder.d.ts +3 -0
- package/dist/editor/page-viewer/pageModelSkeletonBuilder.js +796 -0
- package/dist/editor/page-viewer/pageModelSkeletonBuilder.js.map +1 -0
- package/dist/editor/page-viewer/pageViewContext.d.ts +0 -32
- package/dist/editor/page-viewer/pageViewContext.js +6 -37
- package/dist/editor/page-viewer/pageViewContext.js.map +1 -1
- package/dist/editor/reviews/Comment.d.ts +1 -2
- package/dist/editor/reviews/Comment.js +4 -9
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/CommentEditor.js +1 -1
- package/dist/editor/reviews/CommentEditor.js.map +1 -1
- package/dist/editor/reviews/CommentPopover.js +9 -68
- package/dist/editor/reviews/CommentPopover.js.map +1 -1
- package/dist/editor/reviews/CommentView.js +4 -24
- package/dist/editor/reviews/CommentView.js.map +1 -1
- package/dist/editor/reviews/Comments.d.ts +2 -0
- package/dist/editor/reviews/Comments.js +30 -29
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/FeedbackCard.d.ts +2 -4
- package/dist/editor/reviews/FeedbackCard.js +5 -5
- package/dist/editor/reviews/FeedbackCard.js.map +1 -1
- package/dist/editor/reviews/SuggestedEdit.js +6 -4
- package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
- package/dist/editor/reviews/SuggestionDisplayPopover.js +2 -3
- package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -1
- package/dist/editor/reviews/feedbackSelection.js +4 -32
- package/dist/editor/reviews/feedbackSelection.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +0 -14
- package/dist/editor/services/agentService.js +1 -11
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/reviewsService.d.ts +2 -2
- package/dist/editor/services/reviewsService.js.map +1 -1
- package/dist/editor/settings/SettingsView.js +2 -2
- package/dist/editor/settings/SettingsView.js.map +1 -1
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js +1 -1
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
- package/dist/editor/settings/panels/ProvidersPanel.js +3 -2
- package/dist/editor/settings/panels/ProvidersPanel.js.map +1 -1
- package/dist/editor/sidebar/MorePanelsButton.js +1 -1
- package/dist/editor/sidebar/MorePanelsButton.js.map +1 -1
- package/dist/editor/sidebar/Workbox.js +1 -1
- package/dist/editor/sidebar/Workbox.js.map +1 -1
- package/dist/editor/ui/IconSelectorDialog.js +1 -1
- package/dist/editor/ui/IconSelectorDialog.js.map +1 -1
- package/dist/editor/ui/SimpleIconButton.d.ts +2 -2
- package/dist/editor/ui/SimpleIconButton.js +1 -1
- package/dist/editor/ui/SimpleIconButton.js.map +1 -1
- package/dist/editor/utils.d.ts +17 -1
- package/dist/editor/utils.js +143 -0
- package/dist/editor/utils.js.map +1 -1
- package/dist/editor/version-diff/versionDiffTargets.d.ts +8 -3
- package/dist/editor/version-diff/versionDiffTargets.js +94 -37
- package/dist/editor/version-diff/versionDiffTargets.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/splash-screen/DialogWrappers.js +2 -2
- package/dist/splash-screen/DialogWrappers.js.map +1 -1
- package/dist/splash-screen/ModernSplashScreen.js +3 -11
- package/dist/splash-screen/ModernSplashScreen.js.map +1 -1
- package/dist/splash-screen/NewPage.js +5 -7
- package/dist/splash-screen/NewPage.js.map +1 -1
- package/dist/splash-screen/OpenPage.js +3 -5
- package/dist/splash-screen/OpenPage.js.map +1 -1
- package/dist/splash-screen/RecentPages.js +3 -3
- package/dist/splash-screen/RecentPages.js.map +1 -1
- package/package.json +1 -2
- package/styles.css +0 -49
- package/dist/editor/ai/terminal/components/AgentEditHistoryButton.d.ts +0 -5
- package/dist/editor/ai/terminal/components/AgentEditHistoryButton.js +0 -12
- package/dist/editor/ai/terminal/components/AgentEditHistoryButton.js.map +0 -1
- package/dist/editor/bridge/BridgeClient.d.ts +0 -80
- package/dist/editor/bridge/BridgeClient.js +0 -417
- package/dist/editor/bridge/BridgeClient.js.map +0 -1
- package/dist/editor/field-types/useFormFieldCaretPresence.d.ts +0 -13
- package/dist/editor/field-types/useFormFieldCaretPresence.js +0 -92
- package/dist/editor/field-types/useFormFieldCaretPresence.js.map +0 -1
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.d.ts +0 -6
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js +0 -123
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js.map +0 -1
- package/dist/editor/page-editor-chrome/useBridgeInlineEditing.d.ts +0 -26
- package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +0 -222
- package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +0 -1
- package/dist/editor/page-viewer/bridgeFieldPatch.d.ts +0 -20
- package/dist/editor/page-viewer/bridgeFieldPatch.js +0 -33
- package/dist/editor/page-viewer/bridgeFieldPatch.js.map +0 -1
- package/dist/editor/reviews/commentTransientSelection.d.ts +0 -23
- package/dist/editor/reviews/commentTransientSelection.js +0 -7
- package/dist/editor/reviews/commentTransientSelection.js.map +0 -1
- package/dist/editor/reviews/feedbackOrdering.d.ts +0 -5
- package/dist/editor/reviews/feedbackOrdering.js +0 -27
- package/dist/editor/reviews/feedbackOrdering.js.map +0 -1
- package/dist/editor/reviews/suggestedEditState.d.ts +0 -12
- package/dist/editor/reviews/suggestedEditState.js +0 -90
- package/dist/editor/reviews/suggestedEditState.js.map +0 -1
- package/dist/editor/reviews/suggestionDisplayValue.d.ts +0 -43
- package/dist/editor/reviews/suggestionDisplayValue.js +0 -93
- package/dist/editor/reviews/suggestionDisplayValue.js.map +0 -1
- package/dist/editor/sidebar/BridgeDiagnostics.d.ts +0 -1
- package/dist/editor/sidebar/BridgeDiagnostics.js +0 -41
- package/dist/editor/sidebar/BridgeDiagnostics.js.map +0 -1
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { MiniMap } from "./MiniMap";
|
|
4
4
|
import { useEditContext, useEditContextRef, useFieldsEditContext, useFieldsEditContextRef, } from "../client/editContext";
|
|
5
5
|
import { useSlotContext } from "../views/editorSlotContext";
|
|
6
6
|
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
|
|
7
7
|
import { PageEditorChrome } from "../page-editor-chrome/PageEditorChrome";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
8
|
+
import { IframeOverlayProvider } from "../page-editor-chrome/overlay/IframeOverlayProvider";
|
|
9
|
+
import { useViewportChangeSignal } from "./pageViewContext";
|
|
10
|
+
import morphdom from "morphdom";
|
|
11
11
|
import uuid from "react-uuid";
|
|
12
12
|
import { cn } from "../../lib/utils";
|
|
13
|
-
import {
|
|
13
|
+
import { findComponentRect, findFieldElement, findNearestEditableComponentId, findParentWithAttribute, getAbsolutePosition, getFieldDescriptorFromElement, findClosestFieldElement, extractItemIdFromItemUri, } from "../utils";
|
|
14
|
+
import { extractDOMSelectionContext } from "../utils/selectionContext";
|
|
14
15
|
import { cleanId } from "../utils/id-helper";
|
|
15
|
-
import { getComponentById } from "../componentTreeHelper";
|
|
16
|
+
import { getAllComponentInstances, getComponentById, } from "../componentTreeHelper";
|
|
16
17
|
import { buildComponentContextMenuItems } from "../ContextMenu";
|
|
17
18
|
import { loadFieldButtons } from "../services/editService";
|
|
18
19
|
import { usePathname } from "../client/navigation";
|
|
@@ -21,263 +22,125 @@ import { FieldActionsOverlay, } from "../FieldActionsOverlay";
|
|
|
21
22
|
import { NoLayout } from "../page-editor-chrome/NoLayout";
|
|
22
23
|
import { Spinner } from "../ui/Spinner";
|
|
23
24
|
import { DeviceToolbar } from "./DeviceToolbar";
|
|
25
|
+
import { buildPageModelSkeleton } from "./pageModelSkeletonBuilder";
|
|
24
26
|
import { toSitecoreDate } from "../utils/sitecoreDate";
|
|
25
|
-
|
|
26
|
-
import { getSuggestionDisplayValue } from "../reviews/suggestionDisplayValue";
|
|
27
|
-
import { buildBridgeFieldPatchPayload, getBridgeFieldPatchValue, } from "./bridgeFieldPatch";
|
|
27
|
+
const EDITOR_CSS_STYLE_ID = "parhelia-editor-css";
|
|
28
28
|
const ZOOM_MIN = 0.25;
|
|
29
29
|
const ZOOM_MAX = 2;
|
|
30
30
|
const ZOOM_STEP = 0.25;
|
|
31
31
|
const ZOOM_TRANSITION_MS = 300;
|
|
32
|
-
const
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
function getBridgeGeometryScrollScale(geometry) {
|
|
42
|
-
return typeof geometry?.scrollScale === "number" &&
|
|
43
|
-
Number.isFinite(geometry.scrollScale) &&
|
|
44
|
-
geometry.scrollScale > 0
|
|
45
|
-
? geometry.scrollScale
|
|
46
|
-
: 1;
|
|
47
|
-
}
|
|
48
|
-
function dispatchBridgeOverlayGeometry(iframe, geometry) {
|
|
49
|
-
const documentSize = getBridgeGeometryDocumentSize(geometry);
|
|
50
|
-
const scrollScale = getBridgeGeometryScrollScale(geometry);
|
|
51
|
-
const detail = {
|
|
52
|
-
scrollLeft: (geometry?.scroll.x ?? 0) * scrollScale,
|
|
53
|
-
scrollTop: (geometry?.scroll.y ?? 0) * scrollScale,
|
|
54
|
-
viewportWidth: geometry?.viewport.width,
|
|
55
|
-
viewportHeight: geometry?.viewport.height,
|
|
56
|
-
scrollWidth: documentSize?.width,
|
|
57
|
-
scrollHeight: documentSize?.height,
|
|
58
|
-
};
|
|
59
|
-
iframe?.dispatchEvent(new CustomEvent(IFRAME_OVERLAY_BRIDGE_GEOMETRY_EVENT, { detail }));
|
|
60
|
-
}
|
|
61
|
-
function getBridgeGeometryDocumentSize(geometry) {
|
|
62
|
-
if (!geometry)
|
|
63
|
-
return undefined;
|
|
64
|
-
const rectScale = geometry.rectScale ?? 1;
|
|
65
|
-
const scrollScale = getBridgeGeometryScrollScale(geometry);
|
|
66
|
-
return {
|
|
67
|
-
width: Math.max(geometry.viewport.width, ...geometry.targets.map((target) => target.rect.right * rectScale + geometry.scroll.x * scrollScale)),
|
|
68
|
-
height: Math.max(geometry.viewport.height, ...geometry.targets.map((target) => target.rect.bottom * rectScale + geometry.scroll.y * scrollScale)),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function bridgeKeysMatch(left, right) {
|
|
72
|
-
const normalizedLeft = normalizeMarkerId(left);
|
|
73
|
-
const normalizedRight = normalizeMarkerId(right);
|
|
74
|
-
return (!!normalizedLeft && !!normalizedRight && normalizedLeft === normalizedRight);
|
|
75
|
-
}
|
|
76
|
-
function bridgeItemMatches(item, descriptor) {
|
|
77
|
-
if (!item)
|
|
78
|
-
return false;
|
|
79
|
-
if (!bridgeKeysMatch(item.id, descriptor.id))
|
|
80
|
-
return false;
|
|
81
|
-
if (item.language &&
|
|
82
|
-
descriptor.language &&
|
|
83
|
-
item.language !== descriptor.language) {
|
|
84
|
-
return false;
|
|
32
|
+
const zoomAnimationFrames = new WeakMap();
|
|
33
|
+
function getAccessibleIframeDocument(iframe, location) {
|
|
34
|
+
if (!iframe)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return iframe.contentDocument || iframe.contentWindow?.document || null;
|
|
85
38
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
item.version !== descriptor.version) {
|
|
89
|
-
return false;
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
90
41
|
}
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
function bridgeTargetToDocumentBounds(geometry, target) {
|
|
94
|
-
const rectScale = geometry.rectScale ?? 1;
|
|
95
|
-
const scrollScale = getBridgeGeometryScrollScale(geometry);
|
|
96
|
-
return {
|
|
97
|
-
x: target.rect.left * rectScale + geometry.scroll.x * scrollScale,
|
|
98
|
-
y: target.rect.top * rectScale + geometry.scroll.y * scrollScale,
|
|
99
|
-
width: target.rect.width * rectScale,
|
|
100
|
-
height: target.rect.height * rectScale,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function findBridgeFieldDocumentBounds(geometry, descriptor) {
|
|
104
|
-
if (!geometry)
|
|
105
|
-
return undefined;
|
|
106
|
-
const target = geometry.targets.find((candidate) => {
|
|
107
|
-
if (candidate.kind !== "field")
|
|
108
|
-
return false;
|
|
109
|
-
return (bridgeKeysMatch(candidate.fieldId, descriptor.fieldId) &&
|
|
110
|
-
bridgeItemMatches(candidate.item, descriptor.item));
|
|
111
|
-
});
|
|
112
|
-
return target?.rect
|
|
113
|
-
? bridgeTargetToDocumentBounds(geometry, target)
|
|
114
|
-
: undefined;
|
|
115
42
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
43
|
+
function getAccessibleIframeLocationHref(iframe) {
|
|
44
|
+
try {
|
|
45
|
+
return iframe?.contentWindow?.location?.href;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
118
48
|
return undefined;
|
|
119
|
-
const target = geometry.targets.find((candidate) => {
|
|
120
|
-
if (candidate.kind !== "component")
|
|
121
|
-
return false;
|
|
122
|
-
return (bridgeKeysMatch(candidate.componentId, componentId) ||
|
|
123
|
-
bridgeKeysMatch(candidate.key, componentId));
|
|
124
|
-
});
|
|
125
|
-
return target?.rect
|
|
126
|
-
? bridgeTargetToDocumentBounds(geometry, target)
|
|
127
|
-
: undefined;
|
|
128
|
-
}
|
|
129
|
-
function scrollBridgeBoundsIntoView(pageViewContext, bounds, currentScroll) {
|
|
130
|
-
const geometry = pageViewContext.bridgeGeometry;
|
|
131
|
-
if (!geometry)
|
|
132
|
-
return false;
|
|
133
|
-
const scrollScale = getBridgeGeometryScrollScale(geometry);
|
|
134
|
-
const currentScrollY = (currentScroll?.y ?? geometry.scroll.y) * scrollScale;
|
|
135
|
-
const viewportHeight = geometry.viewport.height;
|
|
136
|
-
const isInViewport = bounds.y + bounds.height > currentScrollY &&
|
|
137
|
-
bounds.y < currentScrollY + viewportHeight;
|
|
138
|
-
if (isInViewport)
|
|
139
|
-
return false;
|
|
140
|
-
const targetScrollY = bounds.y - viewportHeight / 2 + Math.max(bounds.height, 1) / 2;
|
|
141
|
-
return (pageViewContext.requestBridgeScrollBy?.({
|
|
142
|
-
y: targetScrollY - currentScrollY,
|
|
143
|
-
behavior: "smooth",
|
|
144
|
-
}) ?? false);
|
|
145
|
-
}
|
|
146
|
-
function applyIframeZoom(_iframe, _zoom, _location) {
|
|
147
|
-
// Zoom is applied inside the page host through the bridge setZoom command.
|
|
148
|
-
}
|
|
149
|
-
function clampEditorZoom(value) {
|
|
150
|
-
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value));
|
|
151
|
-
}
|
|
152
|
-
function isLoopbackHost(hostname) {
|
|
153
|
-
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
154
|
-
}
|
|
155
|
-
function alignLoopbackHostToEditor(url) {
|
|
156
|
-
if (isLoopbackHost(window.location.hostname) &&
|
|
157
|
-
isLoopbackHost(url.hostname)) {
|
|
158
|
-
// Sitecore auth cookies are host-scoped, so localhost and 127.0.0.1 are
|
|
159
|
-
// different origins even when they point at the same machine.
|
|
160
|
-
url.hostname = window.location.hostname;
|
|
161
49
|
}
|
|
162
50
|
}
|
|
163
|
-
function
|
|
164
|
-
if (!url)
|
|
165
|
-
return undefined;
|
|
51
|
+
function getAccessibleIframeLocationOrigin(iframe) {
|
|
166
52
|
try {
|
|
167
|
-
|
|
168
|
-
alignLoopbackHostToEditor(parsedUrl);
|
|
169
|
-
return parsedUrl.origin;
|
|
53
|
+
return iframe?.contentWindow?.location?.origin;
|
|
170
54
|
}
|
|
171
55
|
catch {
|
|
172
56
|
return undefined;
|
|
173
57
|
}
|
|
174
58
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
59
|
+
function applyIframeZoom(iframe, zoom, location) {
|
|
60
|
+
const documentElement = getAccessibleIframeDocument(iframe, location)?.documentElement;
|
|
61
|
+
if (!documentElement)
|
|
62
|
+
return;
|
|
63
|
+
const ownerWindow = documentElement.ownerDocument.defaultView ?? window;
|
|
64
|
+
let reduceMotion = false;
|
|
65
|
+
try {
|
|
66
|
+
reduceMotion =
|
|
67
|
+
iframe?.contentWindow?.matchMedia?.("(prefers-reduced-motion: reduce)")
|
|
68
|
+
.matches ?? false;
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
const animationFrame = zoomAnimationFrames.get(documentElement);
|
|
72
|
+
if (animationFrame !== undefined) {
|
|
73
|
+
ownerWindow.cancelAnimationFrame(animationFrame);
|
|
74
|
+
zoomAnimationFrames.delete(documentElement);
|
|
75
|
+
}
|
|
76
|
+
documentElement.style.removeProperty("transition");
|
|
77
|
+
const getCurrentZoom = () => {
|
|
78
|
+
const inlineZoom = Number.parseFloat(documentElement.style.zoom);
|
|
79
|
+
if (Number.isFinite(inlineZoom) && inlineZoom > 0)
|
|
80
|
+
return inlineZoom;
|
|
81
|
+
const computedZoom = Number.parseFloat(ownerWindow.getComputedStyle(documentElement).zoom);
|
|
82
|
+
if (Number.isFinite(computedZoom) && computedZoom > 0) {
|
|
83
|
+
return computedZoom;
|
|
192
84
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
85
|
+
return 1;
|
|
86
|
+
};
|
|
87
|
+
const applyFinalZoom = () => {
|
|
88
|
+
if (zoom === 1) {
|
|
89
|
+
documentElement.style.removeProperty("zoom");
|
|
90
|
+
documentElement.style.removeProperty("transform");
|
|
91
|
+
documentElement.style.removeProperty("transform-origin");
|
|
92
|
+
documentElement.style.removeProperty("overflow");
|
|
93
|
+
documentElement.style.removeProperty("will-change");
|
|
94
|
+
return;
|
|
197
95
|
}
|
|
96
|
+
documentElement.style.zoom = String(zoom);
|
|
97
|
+
documentElement.style.removeProperty("transform");
|
|
98
|
+
documentElement.style.removeProperty("transform-origin");
|
|
99
|
+
documentElement.style.overflow = "auto";
|
|
100
|
+
documentElement.style.removeProperty("will-change");
|
|
198
101
|
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
function getOrderedBridgeComponentIds(geometry, page) {
|
|
203
|
-
const orderedIds = [];
|
|
204
|
-
for (const target of geometry?.targets ?? []) {
|
|
205
|
-
if (target.kind !== "component")
|
|
206
|
-
continue;
|
|
207
|
-
const componentId = resolveBridgeComponentId(target.componentId, page);
|
|
208
|
-
if (!componentId)
|
|
209
|
-
continue;
|
|
210
|
-
if (orderedIds.some((id) => bridgeIdsMatch(id, componentId)))
|
|
211
|
-
continue;
|
|
212
|
-
orderedIds.push(componentId);
|
|
213
|
-
}
|
|
214
|
-
return orderedIds;
|
|
215
|
-
}
|
|
216
|
-
function bridgeIdsMatch(left, right) {
|
|
217
|
-
const normalizedLeft = normalizeMarkerId(left);
|
|
218
|
-
const normalizedRight = normalizeMarkerId(right);
|
|
219
|
-
return (!!normalizedLeft && !!normalizedRight && normalizedLeft === normalizedRight);
|
|
220
|
-
}
|
|
221
|
-
function bridgeComponentSelectionsMatch(left, right) {
|
|
222
|
-
if ((left?.length ?? 0) !== (right?.length ?? 0))
|
|
223
|
-
return false;
|
|
224
|
-
return (left ?? []).every((leftId, index) => bridgeIdsMatch(leftId, right?.[index]));
|
|
225
|
-
}
|
|
226
|
-
function bridgeDescriptorMatchesItem(bridgeItem, item) {
|
|
227
|
-
if (!bridgeItem?.id)
|
|
228
|
-
return true;
|
|
229
|
-
if (!bridgeIdsMatch(bridgeItem.id, item.id))
|
|
230
|
-
return false;
|
|
231
|
-
if (bridgeItem.language &&
|
|
232
|
-
item.language &&
|
|
233
|
-
bridgeItem.language.toLowerCase() !== item.language.toLowerCase()) {
|
|
234
|
-
return false;
|
|
102
|
+
if (reduceMotion) {
|
|
103
|
+
applyFinalZoom();
|
|
104
|
+
return;
|
|
235
105
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return false;
|
|
106
|
+
const startZoom = getCurrentZoom();
|
|
107
|
+
const targetZoom = zoom;
|
|
108
|
+
if (Math.abs(startZoom - targetZoom) < 0.001) {
|
|
109
|
+
applyFinalZoom();
|
|
110
|
+
return;
|
|
242
111
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
bridgeIdsMatch(field.name, fieldId) ||
|
|
248
|
-
bridgeIdsMatch(field.displayName, fieldId));
|
|
249
|
-
}
|
|
250
|
-
function bridgeFieldMatchesChangedField(bridgeFieldId, changedFieldId, field) {
|
|
251
|
-
return (bridgeIdsMatch(bridgeFieldId, changedFieldId) ||
|
|
252
|
-
bridgeIdsMatch(bridgeFieldId, field.id) ||
|
|
253
|
-
bridgeIdsMatch(bridgeFieldId, field.name) ||
|
|
254
|
-
bridgeIdsMatch(bridgeFieldId, field.displayName));
|
|
255
|
-
}
|
|
256
|
-
function findBridgeFieldTargetAtPoint(geometry, clientX, clientY) {
|
|
257
|
-
if (!geometry || typeof clientX !== "number" || typeof clientY !== "number") {
|
|
258
|
-
return undefined;
|
|
112
|
+
documentElement.style.willChange = "zoom";
|
|
113
|
+
documentElement.style.zoom = String(startZoom);
|
|
114
|
+
if (targetZoom !== 1) {
|
|
115
|
+
documentElement.style.overflow = "auto";
|
|
259
116
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
|
|
117
|
+
documentElement.style.removeProperty("transform");
|
|
118
|
+
documentElement.style.removeProperty("transform-origin");
|
|
119
|
+
const startedAt = ownerWindow.performance.now();
|
|
120
|
+
const easeInOut = (value) => value < 0.5
|
|
121
|
+
? 4 * value * value * value
|
|
122
|
+
: 1 - Math.pow(-2 * value + 2, 3) / 2;
|
|
123
|
+
const step = (now) => {
|
|
124
|
+
const progress = Math.min(1, (now - startedAt) / ZOOM_TRANSITION_MS);
|
|
125
|
+
const easedProgress = easeInOut(progress);
|
|
126
|
+
const currentZoom = startZoom + (targetZoom - startZoom) * easedProgress;
|
|
127
|
+
documentElement.style.zoom = String(currentZoom);
|
|
128
|
+
documentElement.style.removeProperty("transform");
|
|
129
|
+
documentElement.style.removeProperty("transform-origin");
|
|
130
|
+
if (targetZoom !== 1) {
|
|
131
|
+
documentElement.style.overflow = "auto";
|
|
132
|
+
}
|
|
133
|
+
if (progress < 1) {
|
|
134
|
+
zoomAnimationFrames.set(documentElement, ownerWindow.requestAnimationFrame(step));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
zoomAnimationFrames.delete(documentElement);
|
|
138
|
+
applyFinalZoom();
|
|
139
|
+
};
|
|
140
|
+
zoomAnimationFrames.set(documentElement, ownerWindow.requestAnimationFrame(step));
|
|
272
141
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
return !!((keyboardInteraction.ctrlKey || keyboardInteraction.metaKey) &&
|
|
276
|
-
(keyboardInteraction.key === "." ||
|
|
277
|
-
keyboardInteraction.key === "Period" ||
|
|
278
|
-
keyboardInteraction.key === "Decimal" ||
|
|
279
|
-
keyboardInteraction.code === "Period" ||
|
|
280
|
-
keyboardInteraction.code === "NumpadDecimal"));
|
|
142
|
+
function clampEditorZoom(value) {
|
|
143
|
+
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value));
|
|
281
144
|
}
|
|
282
145
|
export function PageViewerFrame(props) {
|
|
283
146
|
const editContext = useEditContext();
|
|
@@ -297,36 +160,17 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
297
160
|
const editContextRef = useEditContextRef();
|
|
298
161
|
const fieldsContextRef = useFieldsEditContextRef();
|
|
299
162
|
const iframeRef = useRef(null);
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const bridgeTextRangeSourcesRef = useRef(new Map());
|
|
304
|
-
const activeBridgeInlineEditRef = useRef(null);
|
|
305
|
-
const latestBridgeScrollRef = useRef(undefined);
|
|
306
|
-
const suppressNextSelectionScrollRef = useRef(false);
|
|
307
|
-
// Tracks the selection we last evaluated for auto-scroll. The selection
|
|
308
|
-
// effect also depends on focusedField, so it re-runs when a field blurs
|
|
309
|
-
// (focusedField -> undefined) even though the selection itself did not
|
|
310
|
-
// change. Without this guard that blur re-render would scroll the just
|
|
311
|
-
// clicked component into view after the suppress flag was already consumed.
|
|
312
|
-
const lastScrolledSelectionKeyRef = useRef("");
|
|
163
|
+
const currentLoadRef = useRef(null);
|
|
164
|
+
const rebindIframeInteractionsRef = useRef(null);
|
|
165
|
+
const prevModeRef = useRef(editContext.mode);
|
|
313
166
|
const [showSpinner, setShowSpinner] = useState(false);
|
|
314
167
|
const [scroll, setScroll] = useState(0);
|
|
315
168
|
const [showMiniMap, setShowMiniMap] = useState(false);
|
|
316
|
-
const [bridgeGeometryRevision, setBridgeGeometryRevision] = useState(0);
|
|
317
|
-
const [bridgeDomRevision, setBridgeDomRevision] = useState(0);
|
|
318
|
-
const bridgeGeometryRevisionRafRef = useRef(null);
|
|
319
|
-
const bridgeDomRevisionRafRef = useRef(null);
|
|
320
169
|
const fieldActionsOverlay = useRef(null);
|
|
321
|
-
const lastSentBridgeCaretRef = useRef(null);
|
|
322
170
|
const [contextMenuFieldButtons, setContextMenuFieldButtons] = useState([]);
|
|
323
171
|
const [contextMenuField, setContextMenuField] = useState();
|
|
324
172
|
const [contextMenuPosition, setContextMenuPosition] = useState();
|
|
325
173
|
const [preSelectedAction, setPreSelectedAction] = useState();
|
|
326
|
-
const bindIframeRef = useCallback((node) => {
|
|
327
|
-
iframeRef.current = node;
|
|
328
|
-
setIframeElement(node);
|
|
329
|
-
}, []);
|
|
330
174
|
// Clear preSelectedAction when overlay is closed
|
|
331
175
|
useEffect(() => {
|
|
332
176
|
if (editContext?.currentOverlay !==
|
|
@@ -335,6 +179,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
335
179
|
}
|
|
336
180
|
}, [editContext?.currentOverlay, contextMenuField?.fieldId]);
|
|
337
181
|
const zoom = pageViewContext.zoom;
|
|
182
|
+
const blockBlurEventRef = useRef(0);
|
|
338
183
|
const [currentItemDescriptor, setCurrentItemDescriptor] = useState(undefined);
|
|
339
184
|
// Field action handlers for the overlay
|
|
340
185
|
const handleActionClick = async (action, event) => {
|
|
@@ -350,1372 +195,1269 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
350
195
|
// Note: handleParameterizedActionFromContextMenu is now created inline in handleContextMenu
|
|
351
196
|
// to avoid React state timing issues with contextMenuPosition
|
|
352
197
|
const pageItemDescriptor = pageViewContext.pageItemDescriptor;
|
|
353
|
-
|
|
354
|
-
const editor = editContextRef.current;
|
|
355
|
-
return !!(showMiniMap &&
|
|
356
|
-
editor?.showMinimap &&
|
|
357
|
-
!editor?.isMobile &&
|
|
358
|
-
editor?.parheliaSettings?.showMinimap !== false);
|
|
359
|
-
}, [showMiniMap]);
|
|
360
|
-
const updateScrollPosition = useCallback((e) => {
|
|
361
|
-
if (!shouldTrackMinimapScroll())
|
|
362
|
-
return;
|
|
363
|
-
setScroll(e);
|
|
364
|
-
if (!compareView)
|
|
365
|
-
pageViewContextRef.current?.setScroll(e);
|
|
366
|
-
}, [compareView, shouldTrackMinimapScroll]);
|
|
367
|
-
const scrollHandler = useThrottledCallback(updateScrollPosition, 100);
|
|
368
|
-
const scrollHandlerRef = useRef(scrollHandler);
|
|
198
|
+
// Update the context whenever the iframe ref changes
|
|
369
199
|
useEffect(() => {
|
|
370
|
-
|
|
371
|
-
}, [
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
200
|
+
pageViewContext.setEditorIframe(iframeRef.current);
|
|
201
|
+
}, [iframeRef.current, pageViewContext.setEditorIframe]);
|
|
202
|
+
const updateMiniMapVisibility = useDebouncedCallback(() => {
|
|
203
|
+
if (!iframeRef.current)
|
|
374
204
|
return;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
});
|
|
379
|
-
}, []);
|
|
380
|
-
const scheduleBridgeDomRevision = useCallback(() => {
|
|
381
|
-
if (bridgeDomRevisionRafRef.current != null)
|
|
205
|
+
const iframe = iframeRef.current;
|
|
206
|
+
const doc = getAccessibleIframeDocument(iframe, "PageViewerFrame.tsx:updateMiniMapVisibility");
|
|
207
|
+
if (!doc?.documentElement)
|
|
382
208
|
return;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
209
|
+
const scrollContainer = doc.scrollingElement || doc.body;
|
|
210
|
+
if (!scrollContainer)
|
|
211
|
+
return;
|
|
212
|
+
const contentHeight = scrollContainer.scrollHeight;
|
|
213
|
+
const clientHeight = iframe.clientHeight;
|
|
214
|
+
const upperThreshold = clientHeight + 100; // show minimap if content exceeds this height
|
|
215
|
+
const lowerThreshold = clientHeight; // hide minimap if content falls below this
|
|
216
|
+
// Check if minimap is enabled in settings and user controls
|
|
217
|
+
const minimapEnabled = editContext.parheliaSettings?.showMinimap !== false &&
|
|
218
|
+
editContext.showMinimap;
|
|
219
|
+
if (showMiniMap) {
|
|
220
|
+
if (contentHeight <= lowerThreshold || !minimapEnabled) {
|
|
221
|
+
setShowMiniMap(false);
|
|
392
222
|
}
|
|
393
|
-
|
|
394
|
-
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (contentHeight > upperThreshold && minimapEnabled) {
|
|
226
|
+
setShowMiniMap(true);
|
|
395
227
|
}
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return (bridgeClientRef.current?.sendCommand("beginInlineEdit", payload) ??
|
|
403
|
-
false);
|
|
404
|
-
}, []);
|
|
405
|
-
const sendApplyRichTextCommand = useCallback((payload) => {
|
|
406
|
-
return (bridgeClientRef.current?.sendCommand("applyRichTextCommand", payload) ??
|
|
407
|
-
false);
|
|
408
|
-
}, []);
|
|
409
|
-
const { beginInlineEdit: beginBridgeInlineEdit, clearInlineDedupe: clearBridgeInlineDedupe, endFieldFocus: endBridgeFieldFocus, handleFieldValueChanged: handleBridgeFieldValueChanged, handleInlineEditEnded: handleBridgeInlineEditEnded, } = useBridgeInlineEditing({
|
|
410
|
-
pageViewContextRef,
|
|
411
|
-
activeBridgeInlineEditRef,
|
|
412
|
-
blockIframeBlurUntil,
|
|
413
|
-
sendBeginInlineEdit,
|
|
228
|
+
}
|
|
229
|
+
}, 100);
|
|
230
|
+
updateMiniMapVisibility();
|
|
231
|
+
const buildPageModelThrottled = useThrottledCallback(buildPageModelSkeleton, 200, {
|
|
232
|
+
leading: true,
|
|
233
|
+
trailing: true,
|
|
414
234
|
});
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
235
|
+
const requestPageModelBuild = (doc) => {
|
|
236
|
+
if (!doc)
|
|
237
|
+
return;
|
|
238
|
+
buildPageModelThrottled(doc, editContextRef, pageViewContextRef);
|
|
239
|
+
};
|
|
240
|
+
const [iframeSrc, setIframeSrc] = useState();
|
|
241
|
+
const editorCssGuardRef = useRef({ doc: null, observer: null, editMode: true });
|
|
242
|
+
const ensureEditorCssGuard = (doc, editMode) => {
|
|
243
|
+
if (!doc)
|
|
244
|
+
return;
|
|
245
|
+
// Keep latest mode so the observer can re-apply the right CSS after hydration/head updates.
|
|
246
|
+
editorCssGuardRef.current.editMode = editMode;
|
|
247
|
+
const needsNewObserver = editorCssGuardRef.current.doc !== doc;
|
|
248
|
+
if (needsNewObserver) {
|
|
249
|
+
try {
|
|
250
|
+
editorCssGuardRef.current.observer?.disconnect();
|
|
251
|
+
}
|
|
252
|
+
catch { }
|
|
253
|
+
editorCssGuardRef.current.doc = doc;
|
|
254
|
+
editorCssGuardRef.current.observer = null;
|
|
255
|
+
// Guard against frameworks (Next/Head managers) removing/replacing our injected <style>.
|
|
256
|
+
let isApplying = false;
|
|
257
|
+
const observer = new MutationObserver(() => {
|
|
258
|
+
const currentDoc = editorCssGuardRef.current.doc;
|
|
259
|
+
if (!currentDoc || currentDoc !== doc)
|
|
260
|
+
return;
|
|
261
|
+
if (isApplying)
|
|
262
|
+
return;
|
|
263
|
+
const head = currentDoc.head;
|
|
264
|
+
if (!head)
|
|
265
|
+
return;
|
|
266
|
+
const style = head.querySelector(`#${EDITOR_CSS_STYLE_ID}`);
|
|
267
|
+
if (style)
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
isApplying = true;
|
|
271
|
+
injectEditorCSS(currentDoc, editorCssGuardRef.current.editMode);
|
|
430
272
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
273
|
+
finally {
|
|
274
|
+
isApplying = false;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// Observe <head> for style removals, and <html> for rare head replacements.
|
|
278
|
+
try {
|
|
279
|
+
if (doc.head)
|
|
280
|
+
observer.observe(doc.head, { childList: true });
|
|
281
|
+
if (doc.documentElement)
|
|
282
|
+
observer.observe(doc.documentElement, { childList: true });
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
editorCssGuardRef.current.observer = observer;
|
|
438
286
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}, [endBridgeFieldFocus]);
|
|
445
|
-
useEffect(() => {
|
|
446
|
-
const handleBridgeInlineEditRelease = (event) => {
|
|
447
|
-
const detail = event
|
|
448
|
-
.detail;
|
|
449
|
-
const field = detail?.field;
|
|
450
|
-
const activeEdit = activeBridgeInlineEditRef.current;
|
|
451
|
-
if (!field || !activeEdit)
|
|
452
|
-
return;
|
|
453
|
-
if (!bridgeKeysMatch(activeEdit.fieldId, field.fieldId))
|
|
454
|
-
return;
|
|
455
|
-
if (activeEdit.item && !bridgeItemMatches(activeEdit.item, field.item)) {
|
|
456
|
-
return;
|
|
287
|
+
// Apply now, and again shortly after to catch post-hydration head normalization.
|
|
288
|
+
injectEditorCSS(doc, editMode);
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
if (editorCssGuardRef.current.doc === doc) {
|
|
291
|
+
injectEditorCSS(doc, editorCssGuardRef.current.editMode);
|
|
457
292
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
293
|
+
}, 0);
|
|
294
|
+
};
|
|
295
|
+
// Disconnect the CSS guard on unmount.
|
|
296
|
+
useEffect(() => {
|
|
462
297
|
return () => {
|
|
463
|
-
|
|
298
|
+
try {
|
|
299
|
+
editorCssGuardRef.current.observer?.disconnect();
|
|
300
|
+
}
|
|
301
|
+
catch { }
|
|
302
|
+
editorCssGuardRef.current.observer = null;
|
|
303
|
+
editorCssGuardRef.current.doc = null;
|
|
464
304
|
};
|
|
465
305
|
}, []);
|
|
306
|
+
// If the editor mode flips (edit/preview), re-apply CSS and disable inline editing in the iframe.
|
|
466
307
|
useEffect(() => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
clearBridgeInlineDedupe();
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
}, [
|
|
478
|
-
clearBridgeInlineDedupe,
|
|
479
|
-
editContext,
|
|
480
|
-
endBridgeFieldFocus,
|
|
481
|
-
fieldsContextRef,
|
|
482
|
-
]);
|
|
483
|
-
const getBridgePatchDisplayValue = useCallback(({ field, structureField, itemDescriptor, preferRepositoryValue, }) => {
|
|
484
|
-
const showSuggestions = editContext.mode === "suggestions" || editContext.showSuggestedEdits;
|
|
485
|
-
const repositoryValue = getBridgeFieldPatchValue(field);
|
|
486
|
-
const display = getSuggestionDisplayValue({
|
|
487
|
-
repositoryValue,
|
|
488
|
-
modifiedFields: preferRepositoryValue
|
|
489
|
-
? undefined
|
|
490
|
-
: fieldsContext?.modifiedFields,
|
|
491
|
-
suggestedEdits: editContext.suggestedEdits,
|
|
492
|
-
field: {
|
|
493
|
-
fieldId: field.id || structureField.fieldId,
|
|
494
|
-
itemId: itemDescriptor.id,
|
|
495
|
-
language: itemDescriptor.language,
|
|
496
|
-
version: itemDescriptor.version,
|
|
497
|
-
pageItemId: pageViewContext.pageItemDescriptor?.id,
|
|
498
|
-
pageItemVersion: pageViewContext.pageItemDescriptor?.version,
|
|
499
|
-
},
|
|
500
|
-
mode: showSuggestions ? "suggestions" : "baseline",
|
|
501
|
-
});
|
|
502
|
-
return {
|
|
503
|
-
value: showSuggestions ? display.mergedValue : display.baselineValue,
|
|
504
|
-
source: showSuggestions && display.hasSuggestions
|
|
505
|
-
? "suggestion-preview"
|
|
506
|
-
: "real",
|
|
507
|
-
};
|
|
508
|
-
}, [
|
|
509
|
-
editContext.mode,
|
|
510
|
-
editContext.showSuggestedEdits,
|
|
511
|
-
editContext.suggestedEdits,
|
|
512
|
-
fieldsContext?.modifiedFields,
|
|
513
|
-
pageViewContext.pageItemDescriptor,
|
|
514
|
-
]);
|
|
515
|
-
const buildBridgeFieldPatch = useCallback(({ field, structureField, itemDescriptor, preferRepositoryValue, }) => {
|
|
516
|
-
const display = getBridgePatchDisplayValue({
|
|
517
|
-
field,
|
|
518
|
-
structureField,
|
|
519
|
-
itemDescriptor,
|
|
520
|
-
preferRepositoryValue,
|
|
521
|
-
});
|
|
522
|
-
const activeInlineEdit = activeBridgeInlineEditRef.current;
|
|
523
|
-
const patch = buildBridgeFieldPatchPayload({
|
|
524
|
-
field,
|
|
525
|
-
structureField,
|
|
526
|
-
display,
|
|
527
|
-
activeInlineEdit,
|
|
528
|
-
});
|
|
529
|
-
return patch;
|
|
530
|
-
}, [getBridgePatchDisplayValue]);
|
|
531
|
-
const clearBridgePatchSignatures = useCallback(() => {
|
|
532
|
-
bridgePatchSignatureRef.current.clear();
|
|
533
|
-
}, []);
|
|
534
|
-
const sendBridgeFieldPatch = useCallback((bridge, patch, observedTextContent) => {
|
|
535
|
-
const cacheKey = `${patch.elementKey}:${patch.fieldId}`;
|
|
536
|
-
const signature = JSON.stringify({
|
|
537
|
-
value: patch.value,
|
|
538
|
-
isRichText: !!patch.isRichText,
|
|
539
|
-
source: patch.source ?? "",
|
|
540
|
-
observedTextContent: observedTextContent ?? "",
|
|
541
|
-
});
|
|
542
|
-
if (bridgePatchSignatureRef.current.get(cacheKey) === signature) {
|
|
543
|
-
return false;
|
|
308
|
+
const doc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:mode-css");
|
|
309
|
+
const isPreview = editContext.mode === "preview";
|
|
310
|
+
if (doc)
|
|
311
|
+
ensureEditorCssGuard(doc, !isPreview);
|
|
312
|
+
if (isPreview) {
|
|
313
|
+
editContextRef.current?.setSelectedRange(undefined);
|
|
314
|
+
fieldsContextRef.current?.setInlineEditingFieldElement(undefined);
|
|
544
315
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const editor = editContextRef.current;
|
|
551
|
-
if (!editor || lastSentBridgeCaretRef.current === "clear")
|
|
552
|
-
return;
|
|
553
|
-
editor.sendSocketMessage({
|
|
554
|
-
type: "caret-position",
|
|
555
|
-
payload: { offset: null },
|
|
556
|
-
});
|
|
557
|
-
lastSentBridgeCaretRef.current = "clear";
|
|
558
|
-
}, [editContextRef]);
|
|
559
|
-
const sendBridgeRemoteCaretPosition = useCallback((selection) => {
|
|
560
|
-
const editor = editContextRef.current;
|
|
561
|
-
const activeField = selection.activeField;
|
|
562
|
-
const offset = selection.startOffset ?? selection.endOffset;
|
|
563
|
-
if (!editor ||
|
|
564
|
-
!activeField?.fieldId ||
|
|
565
|
-
!selection.collapsed ||
|
|
566
|
-
offset == null) {
|
|
567
|
-
clearBridgeRemoteCaretPosition();
|
|
316
|
+
}, [editContext.mode]);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!pageItemDescriptor ||
|
|
319
|
+
!pageViewContext.editUrl ||
|
|
320
|
+
!pageViewContext.previewUrl)
|
|
568
321
|
return;
|
|
322
|
+
const urlPath = editContext.mode === "preview"
|
|
323
|
+
? pageViewContext.previewUrl
|
|
324
|
+
: pageViewContext.editUrl;
|
|
325
|
+
const prevMode = prevModeRef.current;
|
|
326
|
+
prevModeRef.current = editContext.mode;
|
|
327
|
+
const modeOnlyChange = prevMode !== editContext.mode &&
|
|
328
|
+
prevMode !== "preview" &&
|
|
329
|
+
editContext.mode !== "preview";
|
|
330
|
+
let savedScrollY = 0;
|
|
331
|
+
try {
|
|
332
|
+
savedScrollY = iframeRef.current?.contentWindow?.scrollY ?? 0;
|
|
569
333
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
pageViewContextRef.current?.pageItemDescriptor;
|
|
573
|
-
const version = typeof sourceItem?.version === "number" &&
|
|
574
|
-
Number.isFinite(sourceItem.version)
|
|
575
|
-
? sourceItem.version
|
|
576
|
-
: undefined;
|
|
577
|
-
if (!sourceItem?.id || !sourceItem.language || version === undefined) {
|
|
578
|
-
clearBridgeRemoteCaretPosition();
|
|
579
|
-
return;
|
|
334
|
+
catch {
|
|
335
|
+
savedScrollY = 0;
|
|
580
336
|
}
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (lastSentBridgeCaretRef.current === nextKey)
|
|
587
|
-
return;
|
|
588
|
-
editor.sendSocketMessage({
|
|
589
|
-
type: "caret-position",
|
|
590
|
-
payload: {
|
|
591
|
-
fieldId: activeField.fieldId,
|
|
592
|
-
item,
|
|
593
|
-
offset,
|
|
594
|
-
},
|
|
595
|
-
});
|
|
596
|
-
lastSentBridgeCaretRef.current = nextKey;
|
|
597
|
-
}, [clearBridgeRemoteCaretPosition, editContextRef, pageViewContextRef]);
|
|
598
|
-
const handleBridgeSelection = useCallback((selection, iframe) => {
|
|
599
|
-
const editor = editContextRef.current;
|
|
600
|
-
if (!editor)
|
|
601
|
-
return;
|
|
602
|
-
const isInlineAiUiFocused = () => {
|
|
603
|
-
const activeEl = document.activeElement;
|
|
604
|
-
return !!(activeEl?.closest(".agent-inline-dialog") ||
|
|
605
|
-
activeEl?.closest(".agent-inline-trigger") ||
|
|
606
|
-
activeEl?.closest('[role="dialog"]'));
|
|
607
|
-
};
|
|
608
|
-
if (selection.collapsed ||
|
|
609
|
-
!selection.text ||
|
|
610
|
-
!selection.activeField?.fieldId) {
|
|
611
|
-
sendBridgeRemoteCaretPosition(selection);
|
|
612
|
-
if (!isInlineAiUiFocused()) {
|
|
613
|
-
editor.setSelectedRange(undefined);
|
|
614
|
-
}
|
|
615
|
-
return;
|
|
337
|
+
const renderUrl = new URL(urlPath, window.location.origin);
|
|
338
|
+
const editRev = uuid();
|
|
339
|
+
renderUrl.searchParams.set("edit_rev", editRev);
|
|
340
|
+
if (editContext.mode !== "preview" && editContext.sessionId) {
|
|
341
|
+
renderUrl.searchParams.set("parhelia_session", editContext.sessionId);
|
|
616
342
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
pageViewContextRef.current?.pageItemDescriptor;
|
|
621
|
-
if (!item?.id) {
|
|
622
|
-
editor.setSelectedRange(undefined);
|
|
623
|
-
return;
|
|
343
|
+
if (editContext.mode === "preview" && editContext.previewDate) {
|
|
344
|
+
renderUrl.searchParams.delete("sc_version");
|
|
345
|
+
renderUrl.searchParams.set("sc_date", toSitecoreDate(editContext.previewDate));
|
|
624
346
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const resolveBridgeFieldDescriptor = useCallback(async (interaction) => {
|
|
660
|
-
const pageView = pageViewContextRef.current;
|
|
661
|
-
const selectedRange = editContextRef.current?.selectedRange;
|
|
662
|
-
const selectedRangeElementKey = selectedRange?.elementKey;
|
|
663
|
-
const selectedRangeMatchesInteraction = !!((interaction.kind === "contextMenu" ||
|
|
664
|
-
interaction.kind === "keydown") &&
|
|
665
|
-
selectedRange?.text &&
|
|
666
|
-
selectedRange.fieldId &&
|
|
667
|
-
((selectedRangeElementKey &&
|
|
668
|
-
interaction.elementKey &&
|
|
669
|
-
selectedRangeElementKey === interaction.elementKey) ||
|
|
670
|
-
bridgeIdsMatch(selectedRange.fieldId, interaction.fieldId) ||
|
|
671
|
-
bridgeIdsMatch(selectedRange.itemId, interaction.item?.id)));
|
|
672
|
-
const fieldId = selectedRangeMatchesInteraction
|
|
673
|
-
? selectedRange?.fieldId
|
|
674
|
-
: interaction.fieldId;
|
|
675
|
-
if (!fieldId)
|
|
676
|
-
return undefined;
|
|
677
|
-
const targetElementKey = selectedRangeMatchesInteraction
|
|
678
|
-
? selectedRangeElementKey
|
|
679
|
-
: interaction.elementKey;
|
|
680
|
-
const structureFields = pageView?.bridgeStructure?.fields ?? [];
|
|
681
|
-
const structureField = structureFields.find((field) => bridgeIdsMatch(field.fieldId, fieldId)) ??
|
|
682
|
-
structureFields.find((field) => targetElementKey && field.elementKey === targetElementKey);
|
|
683
|
-
const fallbackItem = pageView?.page?.item ?? pageView?.pageItemDescriptor;
|
|
684
|
-
const componentId = resolveBridgeComponentId(interaction.componentId ?? structureField?.componentId, pageView?.page);
|
|
685
|
-
const component = componentId && pageView?.page
|
|
686
|
-
? getComponentById(componentId, pageView.page)
|
|
687
|
-
: undefined;
|
|
688
|
-
const candidateItems = [];
|
|
689
|
-
const selectedRangeItem = selectedRangeMatchesInteraction &&
|
|
690
|
-
selectedRange?.itemId &&
|
|
691
|
-
selectedRange.language &&
|
|
692
|
-
typeof selectedRange.version === "number"
|
|
693
|
-
? {
|
|
694
|
-
id: selectedRange.itemId,
|
|
695
|
-
language: selectedRange.language,
|
|
696
|
-
version: selectedRange.version,
|
|
697
|
-
database: fallbackItem?.database,
|
|
698
|
-
}
|
|
699
|
-
: undefined;
|
|
700
|
-
const addCandidateItem = (source) => {
|
|
701
|
-
const itemId = source?.id ?? fallbackItem?.id;
|
|
702
|
-
const language = source?.language ?? fallbackItem?.language;
|
|
703
|
-
const versionValue = source?.version ?? fallbackItem?.version;
|
|
704
|
-
const version = typeof versionValue === "number" && Number.isFinite(versionValue)
|
|
705
|
-
? versionValue
|
|
706
|
-
: undefined;
|
|
707
|
-
if (!itemId || !language || version === undefined)
|
|
708
|
-
return;
|
|
709
|
-
if (candidateItems.some((item) => bridgeIdsMatch(item.id, itemId) &&
|
|
710
|
-
item.language.toLowerCase() === language.toLowerCase() &&
|
|
711
|
-
item.version === version)) {
|
|
347
|
+
// Layout-mode marker. Only set when editing shared layout, paired with the
|
|
348
|
+
// `parhelia` editor marker so ParheliaSetLayoutRenderings ignores any unrelated
|
|
349
|
+
// requests that happen to carry parhelia_layout.
|
|
350
|
+
if (editContext.mode !== "preview" && editContext.layoutMode === "shared") {
|
|
351
|
+
renderUrl.searchParams.set("parhelia", "1");
|
|
352
|
+
renderUrl.searchParams.set("parhelia_layout", "shared");
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
renderUrl.searchParams.delete("parhelia_layout");
|
|
356
|
+
}
|
|
357
|
+
// Detect if the version in the URL changed - this requires a full reload, not just requestRefresh
|
|
358
|
+
// because Next.js router.replace may not properly refetch server data for version changes.
|
|
359
|
+
let currentIframeUrl;
|
|
360
|
+
try {
|
|
361
|
+
currentIframeUrl = iframeRef.current?.contentWindow?.location?.href;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
currentIframeUrl = undefined;
|
|
365
|
+
}
|
|
366
|
+
const currentVersion = currentIframeUrl
|
|
367
|
+
? new URL(currentIframeUrl).searchParams.get("sc_version")
|
|
368
|
+
: null;
|
|
369
|
+
const newVersion = renderUrl.searchParams.get("sc_version");
|
|
370
|
+
const versionChanged = (currentVersion !== null || newVersion !== null) &&
|
|
371
|
+
currentVersion !== newVersion;
|
|
372
|
+
const initialLoad = currentItemDescriptor?.id !== pageItemDescriptor.id ||
|
|
373
|
+
currentItemDescriptor?.language !== pageItemDescriptor.language ||
|
|
374
|
+
currentItemDescriptor?.version !== pageItemDescriptor.version;
|
|
375
|
+
const shouldUseIframeSrcReload = initialLoad || pageViewContext.fullscreen;
|
|
376
|
+
const runIntegrationRefresh = (refreshFn, message) => {
|
|
377
|
+
console.log(message);
|
|
378
|
+
const refreshAccepted = refreshFn(renderUrl.toString());
|
|
379
|
+
if (refreshAccepted === false) {
|
|
380
|
+
runFallbackRefresh();
|
|
712
381
|
return;
|
|
713
382
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
383
|
+
const doc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:integration-refresh-doc");
|
|
384
|
+
if (doc) {
|
|
385
|
+
requestPageModelBuild(doc);
|
|
386
|
+
}
|
|
387
|
+
// The integration refresh path may replace/normalize <head> during hydration,
|
|
388
|
+
// which can remove our injected style. Re-ensure a few times post-refresh.
|
|
389
|
+
const ensureLater = (delay) => {
|
|
390
|
+
setTimeout(() => {
|
|
391
|
+
const doc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:integration-refresh-ensure-later");
|
|
392
|
+
if (doc)
|
|
393
|
+
ensureEditorCssGuard(doc, editContext.mode !== "preview");
|
|
394
|
+
rebindIframeInteractionsRef.current?.();
|
|
395
|
+
}, delay);
|
|
396
|
+
};
|
|
397
|
+
ensureLater(0);
|
|
398
|
+
ensureLater(100);
|
|
399
|
+
ensureLater(500);
|
|
400
|
+
ensureLater(1200);
|
|
401
|
+
if (modeOnlyChange && savedScrollY > 0) {
|
|
402
|
+
const restoreScroll = (delay) => {
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
const win = iframeRef.current?.contentWindow;
|
|
405
|
+
if (win && win.scrollY === 0) {
|
|
406
|
+
win.scrollTo(0, savedScrollY);
|
|
407
|
+
}
|
|
408
|
+
}, delay);
|
|
737
409
|
};
|
|
410
|
+
restoreScroll(50);
|
|
411
|
+
restoreScroll(200);
|
|
412
|
+
restoreScroll(500);
|
|
413
|
+
restoreScroll(1000);
|
|
414
|
+
restoreScroll(2000);
|
|
738
415
|
}
|
|
739
|
-
}
|
|
740
|
-
const fallbackDescriptorItem = candidateItems[0];
|
|
741
|
-
if (!fallbackDescriptorItem)
|
|
742
|
-
return undefined;
|
|
743
|
-
return {
|
|
744
|
-
fieldId,
|
|
745
|
-
item: fallbackDescriptorItem,
|
|
746
416
|
};
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
await editor.setFocusedField(field, editor.mode !== "suggestions");
|
|
417
|
+
function runFallbackRefresh() {
|
|
418
|
+
setShowSpinner(true);
|
|
419
|
+
if (shouldUseIframeSrcReload) {
|
|
420
|
+
// For initial loads and fullscreen mode, prefer a full iframe src reload.
|
|
421
|
+
// The morphdom path can race with framework hydration and leave the frame blank.
|
|
422
|
+
console.log(initialLoad
|
|
423
|
+
? "Initial load - setting iframe src"
|
|
424
|
+
: "Fullscreen load - setting iframe src");
|
|
425
|
+
// Clear any stale load tracking to prevent handleLoad from incorrectly
|
|
426
|
+
// skipping spinner dismissal due to revision mismatch
|
|
427
|
+
currentLoadRef.current = null;
|
|
428
|
+
setIframeSrc(renderUrl.toString());
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.log("No integration - reloading frame via fetch");
|
|
432
|
+
// Abort any in-flight load before starting a new one
|
|
433
|
+
try {
|
|
434
|
+
currentLoadRef.current?.controller.abort();
|
|
766
435
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
436
|
+
catch { }
|
|
437
|
+
const controller = new AbortController();
|
|
438
|
+
const expectedRevision = editContext.revision ?? "";
|
|
439
|
+
currentLoadRef.current = { controller, revision: expectedRevision };
|
|
440
|
+
loadContent(renderUrl.toString(), initialLoad, expectedRevision, controller.signal);
|
|
771
441
|
}
|
|
772
|
-
return;
|
|
773
442
|
}
|
|
774
|
-
|
|
775
|
-
|
|
443
|
+
// Use requestRefresh for normal refreshes, but force full reload for version changes.
|
|
444
|
+
// If requestRefresh is temporarily unavailable, wait briefly before falling back.
|
|
445
|
+
let integrationRefreshFn;
|
|
446
|
+
try {
|
|
447
|
+
integrationRefreshFn = iframeRef.current?.contentWindow?.requestRefresh;
|
|
776
448
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (interaction.kind === "contextMenu" && interaction.ctrlKey)
|
|
780
|
-
return;
|
|
781
|
-
const editor = editContextRef.current;
|
|
782
|
-
if (!editor)
|
|
783
|
-
return;
|
|
784
|
-
const selectFromBridgeInteraction = (ids) => {
|
|
785
|
-
if (!bridgeComponentSelectionsMatch(editor.selection, ids)) {
|
|
786
|
-
suppressNextSelectionScrollRef.current = true;
|
|
787
|
-
}
|
|
788
|
-
editor.select(ids);
|
|
789
|
-
};
|
|
790
|
-
let effectiveInteraction = interaction;
|
|
791
|
-
if (interaction.kind === "contextMenu" &&
|
|
792
|
-
!interaction.fieldId &&
|
|
793
|
-
interaction.clientX !== undefined &&
|
|
794
|
-
interaction.clientY !== undefined) {
|
|
795
|
-
const fieldTarget = findBridgeFieldTargetAtPoint(pageViewContextRef.current?.bridgeGeometry, interaction.clientX, interaction.clientY);
|
|
796
|
-
if (fieldTarget?.fieldId) {
|
|
797
|
-
effectiveInteraction = {
|
|
798
|
-
...interaction,
|
|
799
|
-
elementKey: fieldTarget.elementKey ?? interaction.elementKey,
|
|
800
|
-
fieldId: fieldTarget.fieldId,
|
|
801
|
-
};
|
|
802
|
-
}
|
|
449
|
+
catch {
|
|
450
|
+
integrationRefreshFn = undefined;
|
|
803
451
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
effectiveInteraction.fieldId &&
|
|
807
|
-
effectiveInteraction.elementKey &&
|
|
808
|
-
!effectiveInteraction.ctrlKey &&
|
|
809
|
-
!effectiveInteraction.shiftKey &&
|
|
810
|
-
!effectiveInteraction.metaKey);
|
|
811
|
-
const currentOverlay = editor.currentOverlay;
|
|
812
|
-
const isGeneratorOverlay = !!(currentOverlay &&
|
|
813
|
-
typeof currentOverlay === "string" &&
|
|
814
|
-
currentOverlay.endsWith("_generators"));
|
|
815
|
-
if (interaction.kind === "click" && currentOverlay === "context-menu") {
|
|
816
|
-
editor.setCurrentOverlay(undefined);
|
|
452
|
+
if (!versionChanged && integrationRefreshFn) {
|
|
453
|
+
runIntegrationRefresh(integrationRefreshFn, "Integration - requesting refresh");
|
|
817
454
|
}
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
455
|
+
else if (!versionChanged && !shouldUseIframeSrcReload) {
|
|
456
|
+
const retryDelayMs = 150;
|
|
457
|
+
const retryTimer = setTimeout(() => {
|
|
458
|
+
let retryIntegrationRefreshFn;
|
|
459
|
+
try {
|
|
460
|
+
retryIntegrationRefreshFn =
|
|
461
|
+
iframeRef.current?.contentWindow?.requestRefresh;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
retryIntegrationRefreshFn = undefined;
|
|
465
|
+
}
|
|
466
|
+
if (retryIntegrationRefreshFn) {
|
|
467
|
+
runIntegrationRefresh(retryIntegrationRefreshFn, "Integration became available after brief wait - requesting refresh");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
runFallbackRefresh();
|
|
471
|
+
}, retryDelayMs);
|
|
472
|
+
setCurrentItemDescriptor(pageItemDescriptor);
|
|
473
|
+
return () => clearTimeout(retryTimer);
|
|
822
474
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
selectFromBridgeInteraction([]);
|
|
826
|
-
if (isEditableFieldClick) {
|
|
827
|
-
await beginTrackedBridgeInlineEdit(effectiveInteraction);
|
|
828
|
-
}
|
|
829
|
-
else if (interaction.kind === "click") {
|
|
830
|
-
endTrackedBridgeFieldFocus();
|
|
831
|
-
}
|
|
832
|
-
return;
|
|
475
|
+
else {
|
|
476
|
+
runFallbackRefresh();
|
|
833
477
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
.
|
|
851
|
-
if (selectedComponents.length === 0)
|
|
478
|
+
setCurrentItemDescriptor(pageItemDescriptor);
|
|
479
|
+
}, [
|
|
480
|
+
pathname,
|
|
481
|
+
editContext.revision,
|
|
482
|
+
pageItemDescriptor,
|
|
483
|
+
pageViewContext.editUrl,
|
|
484
|
+
pageViewContext.previewUrl,
|
|
485
|
+
pageViewContext.fullscreen,
|
|
486
|
+
editContext.mode,
|
|
487
|
+
editContext.previewDate,
|
|
488
|
+
editContext.sessionId,
|
|
489
|
+
editContext.layoutMode,
|
|
490
|
+
]);
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
if (fieldsContext?.focusedField) {
|
|
493
|
+
if (editContext.selection.length > 0 &&
|
|
494
|
+
fieldsContext.focusedField.item.id !== editContext.selection[0])
|
|
852
495
|
return;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const menuPosition = { x: clientX, y: clientY };
|
|
857
|
-
const adjustedEvent = new MouseEvent("contextmenu", {
|
|
858
|
-
bubbles: true,
|
|
859
|
-
cancelable: true,
|
|
860
|
-
shiftKey: interaction.shiftKey,
|
|
861
|
-
altKey: interaction.altKey,
|
|
862
|
-
ctrlKey: interaction.ctrlKey,
|
|
863
|
-
metaKey: interaction.metaKey,
|
|
864
|
-
view: window,
|
|
865
|
-
clientX,
|
|
866
|
-
clientY,
|
|
867
|
-
button: 2,
|
|
868
|
-
});
|
|
869
|
-
setContextMenuPosition(menuPosition);
|
|
870
|
-
let loadingShown = false;
|
|
871
|
-
const loadingTimer = window.setTimeout(() => {
|
|
872
|
-
loadingShown = true;
|
|
873
|
-
editor.showContextMenu(adjustedEvent, [
|
|
874
|
-
{
|
|
875
|
-
id: "loading",
|
|
876
|
-
label: "Loading...",
|
|
877
|
-
disabled: true,
|
|
878
|
-
icon: _jsx(Spinner, { size: "sm", className: "mr-2" }),
|
|
879
|
-
},
|
|
880
|
-
]);
|
|
881
|
-
}, 100);
|
|
882
|
-
const field = await resolveBridgeFieldDescriptor(effectiveInteraction);
|
|
883
|
-
const fieldButtons = field ? await loadFieldButtons(field) : [];
|
|
884
|
-
const handleParameterizedActionWithPosition = (field, action) => {
|
|
885
|
-
setContextMenuField(field);
|
|
886
|
-
setContextMenuFieldButtons([action]);
|
|
887
|
-
setPreSelectedAction(action);
|
|
888
|
-
window.setTimeout(() => {
|
|
889
|
-
const tempElement = document.createElement("div");
|
|
890
|
-
tempElement.style.position = "fixed";
|
|
891
|
-
tempElement.style.left = menuPosition.x + "px";
|
|
892
|
-
tempElement.style.top = menuPosition.y + "px";
|
|
893
|
-
tempElement.style.width = "1px";
|
|
894
|
-
tempElement.style.height = "1px";
|
|
895
|
-
tempElement.style.visibility = "hidden";
|
|
896
|
-
tempElement.style.pointerEvents = "none";
|
|
897
|
-
document.body.appendChild(tempElement);
|
|
898
|
-
const positionedEvent = new MouseEvent("click", {
|
|
899
|
-
bubbles: true,
|
|
900
|
-
cancelable: true,
|
|
901
|
-
view: window,
|
|
902
|
-
clientX: menuPosition.x,
|
|
903
|
-
clientY: menuPosition.y,
|
|
904
|
-
});
|
|
905
|
-
Object.defineProperty(positionedEvent, "target", {
|
|
906
|
-
value: tempElement,
|
|
907
|
-
enumerable: true,
|
|
908
|
-
});
|
|
909
|
-
fieldActionsOverlay.current?.show(positionedEvent, action);
|
|
910
|
-
window.setTimeout(() => {
|
|
911
|
-
tempElement.remove();
|
|
912
|
-
}, 1000);
|
|
913
|
-
}, 100);
|
|
914
|
-
};
|
|
915
|
-
const items = await buildComponentContextMenuItems(selectedComponents, editor, field, fieldButtons, handleParameterizedActionWithPosition);
|
|
916
|
-
window.clearTimeout(loadingTimer);
|
|
917
|
-
if (loadingShown) {
|
|
918
|
-
editor.updateContextMenu(items);
|
|
496
|
+
let fieldElement;
|
|
497
|
+
try {
|
|
498
|
+
fieldElement = findFieldElement(iframeRef.current, fieldsContext.focusedField);
|
|
919
499
|
}
|
|
920
|
-
|
|
921
|
-
|
|
500
|
+
catch {
|
|
501
|
+
fieldElement = null;
|
|
502
|
+
}
|
|
503
|
+
if (fieldElement) {
|
|
504
|
+
const rect = getAbsolutePosition(fieldElement, iframeRef.current);
|
|
505
|
+
let scrollTop = 0;
|
|
506
|
+
try {
|
|
507
|
+
scrollTop = iframeRef.current?.contentWindow?.scrollY || 0;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
scrollTop = 0;
|
|
511
|
+
}
|
|
512
|
+
const iframeHeight = iframeRef.current?.getBoundingClientRect().height;
|
|
513
|
+
const isInViewport = rect.y + rect.height > scrollTop &&
|
|
514
|
+
rect.y < scrollTop + (iframeHeight || 0);
|
|
515
|
+
if (!isInViewport) {
|
|
516
|
+
try {
|
|
517
|
+
iframeRef.current?.contentWindow?.scrollTo({
|
|
518
|
+
top: rect.y,
|
|
519
|
+
behavior: "smooth",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch { }
|
|
523
|
+
}
|
|
922
524
|
}
|
|
923
|
-
return;
|
|
924
525
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
526
|
+
}, [fieldsContext?.focusedField]);
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
if (!fieldsContext?.focusedField && editContext.selection.length > 0) {
|
|
529
|
+
const lastSelectedComponent = getComponentById(editContext.selection[editContext.selection.length - 1], pageViewContextRef.current.page);
|
|
530
|
+
if (lastSelectedComponent) {
|
|
531
|
+
editContext.setScrollIntoView(lastSelectedComponent.id);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}, [editContext.selection, fieldsContext?.focusedField]);
|
|
535
|
+
const loadContent = async (href, initialLoad, expectedRevision, signal) => {
|
|
536
|
+
console.log("Loading content:", href);
|
|
537
|
+
const start = performance.now();
|
|
538
|
+
if (!href)
|
|
539
|
+
return;
|
|
540
|
+
try {
|
|
541
|
+
const content = await fetch(href, { signal });
|
|
542
|
+
// Detect redirect to login page (session expired)
|
|
543
|
+
if (content.redirected) {
|
|
544
|
+
const redirectUrl = content.url.toLowerCase();
|
|
545
|
+
if (redirectUrl.includes("/identity/login") ||
|
|
546
|
+
redirectUrl.includes("/sitecore/login") ||
|
|
547
|
+
redirectUrl.includes("returnurl=")) {
|
|
548
|
+
console.warn("Session expired - redirected to login page:", content.url);
|
|
549
|
+
setShowSpinner(false);
|
|
550
|
+
// Redirect the main window to the login page
|
|
551
|
+
window.location.href = content.url;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const text = await content.text();
|
|
556
|
+
console.log("Content loaded in " + (performance.now() - start) + " ms");
|
|
557
|
+
// Skip applying if this response is stale
|
|
558
|
+
if (expectedRevision !== (editContextRef.current?.revision ?? "")) {
|
|
559
|
+
console.log("Stale refresh skipped", {
|
|
560
|
+
expectedRevision,
|
|
561
|
+
current: editContextRef.current?.revision,
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
console.log("Content loaded in " + (performance.now() - start) + " ms");
|
|
566
|
+
const doc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:loadContent-doc");
|
|
567
|
+
if (doc) {
|
|
568
|
+
if (initialLoad) {
|
|
569
|
+
// Guard again just before applying
|
|
570
|
+
if (expectedRevision !== (editContextRef.current?.revision ?? "")) {
|
|
571
|
+
console.log("Stale initial load skipped", {
|
|
572
|
+
expectedRevision,
|
|
573
|
+
current: editContextRef.current?.revision,
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
940
576
|
}
|
|
941
|
-
|
|
577
|
+
doc.open();
|
|
578
|
+
doc.write(text);
|
|
579
|
+
doc.close();
|
|
942
580
|
}
|
|
943
581
|
else {
|
|
944
|
-
|
|
582
|
+
const parser = new DOMParser();
|
|
583
|
+
const newDoc = parser.parseFromString(text, "text/html");
|
|
584
|
+
// Guard before morphing DOM to avoid applying stale content
|
|
585
|
+
if (expectedRevision !== (editContextRef.current?.revision ?? "")) {
|
|
586
|
+
console.log("Stale morphdom skipped", {
|
|
587
|
+
expectedRevision,
|
|
588
|
+
current: editContextRef.current?.revision,
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const morphTarget = doc.body && newDoc.body ? "body" : "documentElement";
|
|
593
|
+
if (morphTarget === "body") {
|
|
594
|
+
morphdom(doc.body, newDoc.body);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
morphdom(doc.documentElement, newDoc.documentElement);
|
|
598
|
+
}
|
|
599
|
+
rebindIframeInteractionsRef.current?.();
|
|
600
|
+
try {
|
|
601
|
+
const xa = iframeRef.current.contentWindow.XA;
|
|
602
|
+
if (xa) {
|
|
603
|
+
console.log("init XA");
|
|
604
|
+
xa.init();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch { }
|
|
608
|
+
setShowSpinner(false);
|
|
609
|
+
}
|
|
610
|
+
ensureEditorCssGuard(doc, editContext.mode !== "preview");
|
|
611
|
+
setTimeout(() => {
|
|
612
|
+
injectSXAScripts(iframeRef.current);
|
|
613
|
+
}, 1000);
|
|
614
|
+
try {
|
|
615
|
+
requestPageModelBuild(doc);
|
|
945
616
|
}
|
|
617
|
+
catch (buildErr) { }
|
|
946
618
|
}
|
|
947
|
-
|
|
948
|
-
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
if (err?.name === "AbortError") {
|
|
622
|
+
// Swallow aborts – a newer refresh superseded this one
|
|
623
|
+
return;
|
|
949
624
|
}
|
|
625
|
+
throw err;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
const iframeDoc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:scrollIntoView-doc");
|
|
630
|
+
if (!editContext.scrollIntoView || !iframeDoc?.documentElement)
|
|
950
631
|
return;
|
|
632
|
+
const component = getComponentById(editContext.scrollIntoView, pageViewContextRef.current.page);
|
|
633
|
+
if (!component)
|
|
634
|
+
return;
|
|
635
|
+
let rect;
|
|
636
|
+
try {
|
|
637
|
+
rect = findComponentRect(iframeRef.current, component, true);
|
|
951
638
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
639
|
+
catch {
|
|
640
|
+
rect = undefined;
|
|
641
|
+
}
|
|
642
|
+
if (!rect)
|
|
643
|
+
return;
|
|
644
|
+
if (!iframeRef.current)
|
|
959
645
|
return;
|
|
646
|
+
// Check if element is already in viewport
|
|
647
|
+
const iframeHeight = iframeRef.current.getBoundingClientRect().height;
|
|
648
|
+
let scrollTop = 0;
|
|
649
|
+
try {
|
|
650
|
+
scrollTop = iframeRef.current?.contentWindow?.scrollY || 0;
|
|
960
651
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
await beginTrackedBridgeInlineEdit(effectiveInteraction);
|
|
652
|
+
catch {
|
|
653
|
+
scrollTop = 0;
|
|
964
654
|
}
|
|
965
|
-
|
|
966
|
-
|
|
655
|
+
const elementTop = rect.rect.y;
|
|
656
|
+
const isInViewport = elementTop >= scrollTop && elementTop <= scrollTop + iframeHeight;
|
|
657
|
+
// If already in viewport, no need to scroll
|
|
658
|
+
if (isInViewport) {
|
|
659
|
+
editContext.setScrollIntoView(undefined);
|
|
660
|
+
return;
|
|
967
661
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
662
|
+
const scrollPosition = rect.rect.y - iframeHeight / 2 + rect.rect.height / 2;
|
|
663
|
+
try {
|
|
664
|
+
iframeRef.current?.contentWindow?.scrollTo({
|
|
665
|
+
top: scrollPosition,
|
|
666
|
+
behavior: "smooth",
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch { }
|
|
670
|
+
editContext.setScrollIntoView(undefined);
|
|
671
|
+
}, [editContext.scrollIntoView, pageViewContext.page]);
|
|
977
672
|
useEffect(() => {
|
|
978
|
-
const
|
|
979
|
-
|
|
673
|
+
const handleMessage = (message) => {
|
|
674
|
+
if (message.origin !== window.location.origin)
|
|
675
|
+
return;
|
|
676
|
+
if (message.data.type === "editor-exitFullscreen") {
|
|
677
|
+
pageViewContext.setFullscreen(false);
|
|
678
|
+
}
|
|
679
|
+
if (message.data.type === "editor-timings") {
|
|
680
|
+
editContext.setTimings(message.data.timings);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
window.addEventListener("message", handleMessage);
|
|
684
|
+
return () => {
|
|
685
|
+
window.removeEventListener("message", handleMessage);
|
|
686
|
+
};
|
|
687
|
+
}, []);
|
|
688
|
+
const selecionChangeHandler = useDebouncedCallback(() => {
|
|
689
|
+
const sel = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:selectionchange-doc")?.getSelection();
|
|
690
|
+
const isInlineAiUiFocused = () => {
|
|
691
|
+
const activeEl = document.activeElement;
|
|
692
|
+
return !!(activeEl?.closest(".agent-inline-dialog") ||
|
|
693
|
+
activeEl?.closest(".agent-inline-trigger") ||
|
|
694
|
+
activeEl?.closest('[role="dialog"]'));
|
|
695
|
+
};
|
|
696
|
+
// Whether the inline AI UI is currently mounted (regardless of focus). When the
|
|
697
|
+
// AI dialog is open, the selection may transiently collapse (e.g. a click falling
|
|
698
|
+
// through into the iframe right after opening from the context menu). We must not
|
|
699
|
+
// wipe the selectedRange in that case, otherwise the dialog closes immediately.
|
|
700
|
+
const isInlineAiUiPresent = () => !!document.querySelector(".agent-inline-dialog, .agent-inline-trigger");
|
|
701
|
+
if (!sel || sel.rangeCount === 0) {
|
|
702
|
+
if (!isInlineAiUiFocused() && !isInlineAiUiPresent()) {
|
|
703
|
+
editContextRef.current?.setSelectedRange(undefined);
|
|
704
|
+
}
|
|
980
705
|
return;
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (allowedHostOrigins.length === 0) {
|
|
986
|
-
pageViewContext.setBridgeReady(false);
|
|
987
|
-
pageViewContext.setIframeSupportsRefresh(false);
|
|
706
|
+
}
|
|
707
|
+
// Preserve the last non-collapsed selection while the inline AI UI is open.
|
|
708
|
+
// This avoids losing context when users click the trigger or dialog.
|
|
709
|
+
if (sel.isCollapsed && (isInlineAiUiFocused() || isInlineAiUiPresent())) {
|
|
988
710
|
return;
|
|
989
711
|
}
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
712
|
+
// Find the field element containing the selection/caret
|
|
713
|
+
const fieldElement = findClosestFieldElement(sel.anchorNode);
|
|
714
|
+
if (!fieldElement) {
|
|
715
|
+
if (!isInlineAiUiFocused() && !isInlineAiUiPresent()) {
|
|
716
|
+
editContextRef.current?.setSelectedRange(undefined);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const fieldId = fieldElement.getAttribute("data-fieldid");
|
|
721
|
+
if (!fieldId)
|
|
722
|
+
return;
|
|
723
|
+
const range = sel.getRangeAt(0);
|
|
724
|
+
// Guard: if layout components are hidden, do not set focus for fields of layout components
|
|
725
|
+
if (editContextRef.current?.showLayoutComponents === false &&
|
|
726
|
+
pageViewContextRef.current?.page) {
|
|
727
|
+
const ownerId = fieldElement.getAttribute("data-itemid") || "";
|
|
728
|
+
const owner = getComponentById(ownerId, pageViewContextRef.current.page);
|
|
729
|
+
if (owner?.layoutId)
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
// Compute the global offsets relative to the field element.
|
|
733
|
+
const globalStartOffset = getGlobalTextOffset(fieldElement, range.startContainer, range.startOffset);
|
|
734
|
+
const globalEndOffset = getGlobalTextOffset(fieldElement, range.endContainer, range.endOffset);
|
|
735
|
+
const selectedText = range.toString();
|
|
736
|
+
// Extract plain text context from the DOM
|
|
737
|
+
const { contextBefore, contextAfter } = extractDOMSelectionContext(fieldElement, globalStartOffset, globalEndOffset);
|
|
738
|
+
// Clone the range for the replaceText callback (ranges can become invalid after DOM changes)
|
|
739
|
+
const rangeClone = range.cloneRange();
|
|
740
|
+
const itemId = fieldElement.getAttribute("data-itemid") || "";
|
|
741
|
+
const language = fieldElement.getAttribute("data-language") || "";
|
|
742
|
+
const versionStr = fieldElement.getAttribute("data-version");
|
|
743
|
+
const version = versionStr ? parseInt(versionStr, 10) : 0;
|
|
744
|
+
editContextRef.current?.setSelectedRange({
|
|
745
|
+
itemId,
|
|
746
|
+
fieldId: fieldId,
|
|
747
|
+
language,
|
|
748
|
+
version,
|
|
749
|
+
startOffset: globalStartOffset,
|
|
750
|
+
endOffset: globalEndOffset,
|
|
751
|
+
text: selectedText,
|
|
752
|
+
contextBefore,
|
|
753
|
+
contextAfter,
|
|
754
|
+
// Create a callback that uses DOM Range API to replace text in contenteditable
|
|
755
|
+
replaceText: (newText) => {
|
|
756
|
+
try {
|
|
757
|
+
// Delete the selected content (no-op for collapsed ranges)
|
|
758
|
+
rangeClone.deleteContents();
|
|
759
|
+
// Insert the new text - use the correct document from the range containers
|
|
760
|
+
const doc = rangeClone.startContainer.ownerDocument || document;
|
|
761
|
+
const textNode = doc.createTextNode(newText);
|
|
762
|
+
rangeClone.insertNode(textNode);
|
|
763
|
+
// Collapse the range to the end of the inserted text
|
|
764
|
+
rangeClone.setStartAfter(textNode);
|
|
765
|
+
rangeClone.collapse(true);
|
|
766
|
+
// Update the selection
|
|
767
|
+
const iframeSel = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:replaceText-doc")?.getSelection();
|
|
768
|
+
if (iframeSel) {
|
|
769
|
+
iframeSel.removeAllRanges();
|
|
770
|
+
iframeSel.addRange(rangeClone);
|
|
771
|
+
}
|
|
772
|
+
// Explicitly save the field value since the MutationObserver may not be active
|
|
773
|
+
// when text is selected without entering inline edit mode
|
|
774
|
+
const isRichText = fieldElement?.getAttribute("data-is-richtext") === "true";
|
|
775
|
+
const valueToSave = isRichText
|
|
776
|
+
? fieldElement?.innerHTML
|
|
777
|
+
: fieldElement?.innerText;
|
|
778
|
+
if (fieldId &&
|
|
779
|
+
itemId &&
|
|
780
|
+
language &&
|
|
781
|
+
version &&
|
|
782
|
+
editContextRef.current) {
|
|
783
|
+
editContextRef.current.operations.editField({
|
|
784
|
+
field: {
|
|
785
|
+
fieldId,
|
|
786
|
+
fieldName: fieldElement?.getAttribute("data-fieldname") || undefined,
|
|
787
|
+
item: { id: itemId, language, version },
|
|
1048
788
|
},
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
bridge.sendCommand("setZoom", {
|
|
1053
|
-
zoom: pageViewContextRef.current?.zoom ?? 1,
|
|
789
|
+
originatingSlotId: slotContext?.slotId,
|
|
790
|
+
refresh: "none",
|
|
791
|
+
value: valueToSave,
|
|
1054
792
|
});
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
case "pageSkeletonUpdated":
|
|
1060
|
-
pageViewContext.setPageSkeleton(event.payload.pageSkeleton);
|
|
1061
|
-
break;
|
|
1062
|
-
case "geometryUpdated":
|
|
1063
|
-
latestBridgeScrollRef.current = event.payload.geometry.scroll;
|
|
1064
|
-
pageViewContext.bridgeGeometry = event.payload.geometry;
|
|
1065
|
-
pageViewContext.setBridgeGeometry(event.payload.geometry);
|
|
1066
|
-
scheduleBridgeGeometryRevision();
|
|
1067
|
-
dispatchBridgeOverlayScroll(iframe, event.payload.geometry.scroll, getBridgeGeometryScrollScale(event.payload.geometry));
|
|
1068
|
-
dispatchBridgeOverlayGeometry(iframe, event.payload.geometry);
|
|
1069
|
-
if (shouldTrackMinimapScroll()) {
|
|
1070
|
-
scrollHandlerRef.current(event.payload.geometry.scroll.y);
|
|
1071
|
-
}
|
|
1072
|
-
break;
|
|
1073
|
-
case "domUpdated":
|
|
1074
|
-
pageViewContext.bridgeDom = event.payload;
|
|
1075
|
-
pageViewContext.setBridgeDom(event.payload);
|
|
1076
|
-
scheduleBridgeDomRevision();
|
|
1077
|
-
window.dispatchEvent(new CustomEvent(BRIDGE_DOM_UPDATED_EVENT));
|
|
1078
|
-
break;
|
|
1079
|
-
case "refreshStarted":
|
|
1080
|
-
clearBridgePatchSignatures();
|
|
1081
|
-
activeBridgeInlineEditRef.current = null;
|
|
1082
|
-
break;
|
|
1083
|
-
case "refreshCompleted":
|
|
1084
|
-
// Runtime refresh replaces host DOM without a new bridge ready event.
|
|
1085
|
-
// Field patches sent during the refresh window may have been cached
|
|
1086
|
-
// against the previous DOM generation, so force the refreshed host to
|
|
1087
|
-
// receive the current repository values again.
|
|
1088
|
-
clearBridgePatchSignatures();
|
|
1089
|
-
if (event.payload.structure) {
|
|
1090
|
-
pageViewContext.setBridgeStructure(event.payload.structure);
|
|
1091
|
-
}
|
|
1092
|
-
if (event.payload.geometry) {
|
|
1093
|
-
latestBridgeScrollRef.current = event.payload.geometry.scroll;
|
|
1094
|
-
pageViewContext.bridgeGeometry = event.payload.geometry;
|
|
1095
|
-
pageViewContext.setBridgeGeometry(event.payload.geometry);
|
|
1096
|
-
scheduleBridgeGeometryRevision();
|
|
1097
|
-
dispatchBridgeOverlayScroll(iframe, event.payload.geometry.scroll, getBridgeGeometryScrollScale(event.payload.geometry));
|
|
1098
|
-
dispatchBridgeOverlayGeometry(iframe, event.payload.geometry);
|
|
1099
|
-
if (shouldTrackMinimapScroll()) {
|
|
1100
|
-
scrollHandlerRef.current(event.payload.geometry.scroll.y);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
setShowSpinner(false);
|
|
1104
|
-
break;
|
|
1105
|
-
case "selectionChanged":
|
|
1106
|
-
pageViewContext.setBridgeSelection(event.payload.selection);
|
|
1107
|
-
handleBridgeSelection(event.payload.selection, iframe);
|
|
1108
|
-
break;
|
|
1109
|
-
case "fieldValueChanged":
|
|
1110
|
-
handleBridgeFieldValueChanged(event.payload);
|
|
1111
|
-
break;
|
|
1112
|
-
case "inlineEditEnded":
|
|
1113
|
-
handleBridgeInlineEditEnded(event.payload);
|
|
1114
|
-
activeBridgeInlineEditRef.current = null;
|
|
1115
|
-
break;
|
|
1116
|
-
case "interaction":
|
|
1117
|
-
pageViewContext.setBridgeInteraction(event.payload);
|
|
1118
|
-
void handleBridgeInteraction(event.payload, iframe);
|
|
1119
|
-
break;
|
|
1120
|
-
case "scrollChanged":
|
|
1121
|
-
latestBridgeScrollRef.current = event.payload.scroll;
|
|
1122
|
-
dispatchBridgeOverlayScroll(iframe, event.payload.scroll, getBridgeGeometryScrollScale(pageViewContextRef.current?.bridgeGeometry));
|
|
1123
|
-
if (shouldTrackMinimapScroll()) {
|
|
1124
|
-
scrollHandlerRef.current(event.payload.scroll.y);
|
|
1125
|
-
}
|
|
1126
|
-
break;
|
|
1127
|
-
case "renderError":
|
|
1128
|
-
console.warn("[Parhelia bridge] Host render error", event.payload);
|
|
1129
|
-
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
console.error("[PageViewerFrame] Failed to replace text:", error);
|
|
1130
797
|
}
|
|
1131
798
|
},
|
|
1132
799
|
});
|
|
1133
|
-
|
|
1134
|
-
pageViewContext.setBridgeReady(false);
|
|
1135
|
-
client.connect();
|
|
1136
|
-
return () => {
|
|
1137
|
-
if (bridgeClientRef.current === client) {
|
|
1138
|
-
bridgeClientRef.current = null;
|
|
1139
|
-
}
|
|
1140
|
-
client.disconnect();
|
|
1141
|
-
clearBridgePatchSignatures();
|
|
1142
|
-
activeBridgeInlineEditRef.current = null;
|
|
1143
|
-
pageViewContext.setBridgeReady(false);
|
|
1144
|
-
pageViewContext.setIframeSupportsRefresh(false);
|
|
1145
|
-
};
|
|
1146
|
-
}, [
|
|
1147
|
-
iframeElement,
|
|
1148
|
-
compareView,
|
|
1149
|
-
handleBridgeFieldValueChanged,
|
|
1150
|
-
handleBridgeInlineEditEnded,
|
|
1151
|
-
handleBridgeInteraction,
|
|
1152
|
-
handleBridgeSelection,
|
|
1153
|
-
clearBridgePatchSignatures,
|
|
1154
|
-
pageViewContext.editUrl,
|
|
1155
|
-
pageViewContext.previewUrl,
|
|
1156
|
-
scheduleBridgeDomRevision,
|
|
1157
|
-
scheduleBridgeGeometryRevision,
|
|
1158
|
-
shouldTrackMinimapScroll,
|
|
1159
|
-
]);
|
|
800
|
+
}, 300);
|
|
1160
801
|
useEffect(() => {
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
802
|
+
const iframe = iframeRef.current;
|
|
803
|
+
if (!iframe)
|
|
804
|
+
return;
|
|
805
|
+
let boundDocument = null;
|
|
806
|
+
let boundDocumentElement = null;
|
|
807
|
+
let boundScrollContainer = null;
|
|
808
|
+
let mutationObserver = null;
|
|
809
|
+
const handleIframeMouseDown = async (event) => {
|
|
810
|
+
const target = event.target;
|
|
811
|
+
const targetElement = target;
|
|
812
|
+
if (editContextRef.current?.isRefreshing && showSpinner)
|
|
1167
813
|
return;
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
814
|
+
// Activate the editor slot this iframe belongs to
|
|
815
|
+
const slotElement = iframe?.closest("[data-editor-slot]");
|
|
816
|
+
const slotId = slotElement?.getAttribute("data-slotid");
|
|
817
|
+
const activeSlotId = editContextRef.current?.getActiveSlotId?.() ??
|
|
818
|
+
editContextRef.current?.activeSlotId;
|
|
819
|
+
if (slotId && activeSlotId !== slotId) {
|
|
820
|
+
editContextRef.current?.setActiveSlot(slotId);
|
|
821
|
+
}
|
|
822
|
+
// Skip selection changes on right-click (button 2) - let context menu handler deal with it
|
|
823
|
+
if (event.button === 2)
|
|
1171
824
|
return;
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
825
|
+
const fieldElement = targetElement
|
|
826
|
+
? findParentWithAttribute(targetElement, "data-fieldid")
|
|
827
|
+
: null;
|
|
828
|
+
const pageForSelection = pageViewContextRef.current?.page;
|
|
829
|
+
const fieldDescriptor = fieldElement?.hasAttribute("data-itemid")
|
|
830
|
+
? getFieldDescriptorFromElement(fieldElement)
|
|
831
|
+
: undefined;
|
|
832
|
+
const componentIdFromField = fieldDescriptor && targetElement && pageForSelection
|
|
833
|
+
? resolveComponentIdForFieldTarget(fieldDescriptor, targetElement, pageForSelection)
|
|
834
|
+
: undefined;
|
|
835
|
+
const rawComponentId = componentIdFromField ??
|
|
836
|
+
(targetElement
|
|
837
|
+
? findNearestEditableComponentId(targetElement)
|
|
838
|
+
: undefined);
|
|
839
|
+
let componentId = rawComponentId;
|
|
840
|
+
if (!componentIdFromField && componentId && pageForSelection) {
|
|
841
|
+
componentId = resolveComponentIdForTarget(componentId, targetElement, pageForSelection);
|
|
842
|
+
}
|
|
843
|
+
// Layout components can still be selected even when showLayoutComponents is false
|
|
844
|
+
// They will be displayed in read-only mode
|
|
845
|
+
const currentOverlayName = editContextRef.current?.currentOverlay;
|
|
846
|
+
const isGeneratorOverlay = !!(currentOverlayName &&
|
|
847
|
+
typeof currentOverlayName === "string" &&
|
|
848
|
+
currentOverlayName.endsWith("_generators"));
|
|
849
|
+
// Don't close context-menu overlay on mousedown - let it handle its own closing
|
|
850
|
+
if (editContextRef.current?.currentOverlay &&
|
|
851
|
+
!isGeneratorOverlay &&
|
|
852
|
+
editContextRef.current.currentOverlay !== "context-menu")
|
|
853
|
+
editContextRef.current?.setCurrentOverlay(undefined);
|
|
854
|
+
if (componentId) {
|
|
855
|
+
const currentSelection = editContextRef.current?.selection || [];
|
|
856
|
+
if (event.shiftKey && currentSelection.length > 0) {
|
|
857
|
+
const page = pageViewContextRef.current?.page;
|
|
858
|
+
if (page) {
|
|
859
|
+
// Build ordered list of visible component ids by DOM order
|
|
860
|
+
const doc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:shift-select-doc");
|
|
861
|
+
const orderedIds = [];
|
|
862
|
+
if (doc) {
|
|
863
|
+
const all = Array.from(doc.querySelectorAll("[data-component-id]"));
|
|
864
|
+
for (const el of all) {
|
|
865
|
+
const id = el.getAttribute("data-component-id");
|
|
866
|
+
if (!id)
|
|
867
|
+
continue;
|
|
868
|
+
// Layout components are now selectable even when showLayoutComponents is false
|
|
869
|
+
orderedIds.push(id);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const anchorId = currentSelection[currentSelection.length - 1];
|
|
873
|
+
const aIdx = orderedIds.indexOf(anchorId);
|
|
874
|
+
const bIdx = orderedIds.indexOf(componentId);
|
|
875
|
+
if (aIdx !== -1 && bIdx !== -1) {
|
|
876
|
+
const start = Math.min(aIdx, bIdx);
|
|
877
|
+
const end = Math.max(aIdx, bIdx);
|
|
878
|
+
const range = orderedIds.slice(start, end + 1);
|
|
879
|
+
if (event.ctrlKey) {
|
|
880
|
+
const union = new Set([...currentSelection, ...range]);
|
|
881
|
+
editContextRef.current?.select(Array.from(union));
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
editContextRef.current?.select(range);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
editContextRef.current?.select([componentId]);
|
|
889
|
+
}
|
|
1184
890
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
891
|
+
else {
|
|
892
|
+
editContextRef.current?.select([componentId]);
|
|
1187
893
|
}
|
|
1188
|
-
const patch = buildBridgeFieldPatch({
|
|
1189
|
-
field,
|
|
1190
|
-
structureField,
|
|
1191
|
-
itemDescriptor: change.item,
|
|
1192
|
-
preferRepositoryValue: change.source === "local-field-update",
|
|
1193
|
-
});
|
|
1194
|
-
if (!patch)
|
|
1195
|
-
continue;
|
|
1196
|
-
patchedElementKeys.add(structureField.elementKey);
|
|
1197
|
-
patches.push(patch);
|
|
1198
|
-
}
|
|
1199
|
-
for (const patch of patches) {
|
|
1200
|
-
if (disposed)
|
|
1201
|
-
return;
|
|
1202
|
-
const structureField = structure.fields.find((field) => field.elementKey === patch.elementKey &&
|
|
1203
|
-
field.fieldId === patch.fieldId);
|
|
1204
|
-
sendBridgeFieldPatch(bridge, patch, structureField?.textContent);
|
|
1205
894
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
return;
|
|
1217
|
-
const localField = localFieldsById.get(changedFieldId.toLowerCase());
|
|
1218
|
-
if (!localField) {
|
|
1219
|
-
needsAsyncLookup = true;
|
|
1220
|
-
continue;
|
|
895
|
+
else if (event.ctrlKey) {
|
|
896
|
+
// Toggle selection: add if not selected, remove if already selected
|
|
897
|
+
const idx = currentSelection.indexOf(componentId);
|
|
898
|
+
if (idx === -1) {
|
|
899
|
+
editContextRef.current?.select([...currentSelection, componentId]);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
const newSelection = [...currentSelection];
|
|
903
|
+
newSelection.splice(idx, 1);
|
|
904
|
+
editContextRef.current?.select(newSelection);
|
|
1221
905
|
}
|
|
1222
|
-
processResolvedField(change, changedFieldId, localField);
|
|
1223
906
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
907
|
+
else {
|
|
908
|
+
// Regular click: always select just this component
|
|
909
|
+
editContextRef.current?.select([componentId]);
|
|
1226
910
|
}
|
|
911
|
+
// if (mode !== "edit") {
|
|
912
|
+
//editContextRef.current?.setScrollIntoView(componentId);
|
|
913
|
+
//}
|
|
1227
914
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
915
|
+
else {
|
|
916
|
+
editContextRef.current?.select([]);
|
|
917
|
+
}
|
|
918
|
+
// Skip field focusing when using modifier keys for multi-selection
|
|
919
|
+
if (!event.ctrlKey &&
|
|
920
|
+
!event.shiftKey &&
|
|
921
|
+
((editContextRef.current?.mode === "edit" &&
|
|
922
|
+
pageViewContextRef.current?.page?.item.canWriteItem) ||
|
|
923
|
+
editContextRef.current?.mode === "suggestions")) {
|
|
924
|
+
if (fieldElement?.hasAttribute("data-itemid")) {
|
|
925
|
+
// Guard: if layout components are hidden, do not allow editing fields of layout components
|
|
926
|
+
if (editContextRef.current?.showLayoutComponents === false &&
|
|
927
|
+
pageViewContextRef.current?.page) {
|
|
928
|
+
const owningItemId = fieldElement.getAttribute("data-itemid") || "";
|
|
929
|
+
const ownerComponent = getComponentById(owningItemId, pageViewContextRef.current.page);
|
|
930
|
+
if (ownerComponent?.layoutId) {
|
|
1237
931
|
return;
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
blockBlurEventRef.current = Date.now() + 500;
|
|
935
|
+
const shouldRequestLock = editContextRef.current?.mode !== "suggestions";
|
|
936
|
+
const hasLock = (await fieldsContextRef.current?.setFocusedField(fieldDescriptor, shouldRequestLock)) ?? false;
|
|
937
|
+
if (hasLock &&
|
|
938
|
+
fieldsContextRef.current?.inlineEditingFieldElement !== fieldElement) {
|
|
939
|
+
fieldsContextRef.current?.setInlineEditingFieldElement(fieldElement);
|
|
940
|
+
// Don't prevent default - we want the browser's natural cursor positioning
|
|
1247
941
|
}
|
|
1248
942
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
unsubscribe();
|
|
1258
|
-
};
|
|
1259
|
-
}, [
|
|
1260
|
-
buildBridgeFieldPatch,
|
|
1261
|
-
editContext.itemsRepository,
|
|
1262
|
-
sendBridgeFieldPatch,
|
|
1263
|
-
]);
|
|
1264
|
-
useEffect(() => {
|
|
1265
|
-
const bridge = bridgeClientRef.current;
|
|
1266
|
-
const structure = pageViewContext.bridgeStructure;
|
|
1267
|
-
if (!pageViewContext.bridgeReady || !bridge || !structure?.fields.length) {
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
let disposed = false;
|
|
1271
|
-
void (async () => {
|
|
1272
|
-
const patchedElementKeys = new Set();
|
|
1273
|
-
for (const structureField of structure.fields) {
|
|
1274
|
-
if (disposed)
|
|
1275
|
-
return;
|
|
1276
|
-
if (!structureField.elementKey)
|
|
1277
|
-
continue;
|
|
1278
|
-
if (patchedElementKeys.has(structureField.elementKey))
|
|
1279
|
-
continue;
|
|
1280
|
-
const fallbackItem = pageViewContextRef.current?.pageItemDescriptor ??
|
|
1281
|
-
pageViewContextRef.current?.page?.item?.descriptor;
|
|
1282
|
-
const bridgeItem = structureField.item;
|
|
1283
|
-
const itemDescriptor = {
|
|
1284
|
-
id: bridgeItem?.id ?? fallbackItem?.id,
|
|
1285
|
-
language: bridgeItem?.language ?? fallbackItem?.language,
|
|
1286
|
-
version: typeof bridgeItem?.version === "number"
|
|
1287
|
-
? bridgeItem.version
|
|
1288
|
-
: fallbackItem?.version,
|
|
1289
|
-
};
|
|
1290
|
-
if (!itemDescriptor.id ||
|
|
1291
|
-
!itemDescriptor.language ||
|
|
1292
|
-
typeof itemDescriptor.version !== "number") {
|
|
1293
|
-
continue;
|
|
943
|
+
else {
|
|
944
|
+
// Clicked inside iframe but not on a field: clear focused field
|
|
945
|
+
fieldsContextRef.current?.setFocusedField(undefined, false);
|
|
946
|
+
fieldsContextRef.current?.setInlineEditingFieldElement(undefined);
|
|
947
|
+
// Release field locks when unfocusing field
|
|
948
|
+
if (editContextRef.current?.unlockField) {
|
|
949
|
+
editContextRef.current.unlockField(undefined);
|
|
950
|
+
}
|
|
1294
951
|
}
|
|
1295
|
-
const loadedItem = await editContext.itemsRepository.getItem(itemDescriptor);
|
|
1296
|
-
if (disposed || !loadedItem)
|
|
1297
|
-
continue;
|
|
1298
|
-
const field = loadedItem.fields.find((candidate) => fieldIdentifierMatches(candidate, structureField.fieldId));
|
|
1299
|
-
if (!field)
|
|
1300
|
-
continue;
|
|
1301
|
-
const patch = buildBridgeFieldPatch({
|
|
1302
|
-
field,
|
|
1303
|
-
structureField,
|
|
1304
|
-
itemDescriptor: itemDescriptor,
|
|
1305
|
-
});
|
|
1306
|
-
if (!patch)
|
|
1307
|
-
continue;
|
|
1308
|
-
patchedElementKeys.add(structureField.elementKey);
|
|
1309
|
-
sendBridgeFieldPatch(bridge, patch, structureField.textContent);
|
|
1310
952
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
953
|
+
const clickEvent = new MouseEvent("click", {
|
|
954
|
+
view: window,
|
|
955
|
+
bubbles: true,
|
|
956
|
+
cancelable: true,
|
|
957
|
+
clientX: event.clientX,
|
|
958
|
+
clientY: event.clientY,
|
|
959
|
+
});
|
|
960
|
+
if (!isGeneratorOverlay) {
|
|
961
|
+
document.dispatchEvent(clickEvent);
|
|
1314
962
|
}
|
|
1315
|
-
});
|
|
1316
|
-
return () => {
|
|
1317
|
-
disposed = true;
|
|
1318
963
|
};
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
editContext.itemsRepository.revision,
|
|
1323
|
-
editContext.mode,
|
|
1324
|
-
editContext.showSuggestedEdits,
|
|
1325
|
-
editContext.suggestedEdits,
|
|
1326
|
-
fieldsContext?.modifiedFields,
|
|
1327
|
-
bridgeDomRevision,
|
|
1328
|
-
pageViewContext.bridgeReady,
|
|
1329
|
-
pageViewContext.bridgeStructure,
|
|
1330
|
-
sendBridgeFieldPatch,
|
|
1331
|
-
]);
|
|
1332
|
-
useEffect(() => {
|
|
1333
|
-
const requestBridgeGeometry = (payload) => {
|
|
1334
|
-
const bridge = bridgeClientRef.current;
|
|
1335
|
-
if (!bridge)
|
|
1336
|
-
return false;
|
|
1337
|
-
// The editing host only remembers the most recent queryGeometry payload.
|
|
1338
|
-
// Comments, suggestions, locks and version-diff each contribute text
|
|
1339
|
-
// ranges independently, so without merging they clobbered one another and
|
|
1340
|
-
// fought in an endless re-request loop (heavy highlight flicker). Keep the
|
|
1341
|
-
// latest ranges per `source` and always query with their union.
|
|
1342
|
-
if (payload && typeof payload.source === "string") {
|
|
1343
|
-
const { source, textRanges, ...rest } = payload;
|
|
1344
|
-
const sources = bridgeTextRangeSourcesRef.current;
|
|
1345
|
-
if (textRanges && textRanges.length > 0) {
|
|
1346
|
-
sources.set(source, textRanges);
|
|
1347
|
-
}
|
|
1348
|
-
else {
|
|
1349
|
-
sources.delete(source);
|
|
1350
|
-
}
|
|
1351
|
-
const mergedTextRanges = Array.from(sources.values()).flat();
|
|
1352
|
-
return (bridge.sendCommand("queryGeometry", {
|
|
1353
|
-
...rest,
|
|
1354
|
-
textRanges: mergedTextRanges,
|
|
1355
|
-
}) ?? false);
|
|
1356
|
-
}
|
|
1357
|
-
return bridge.sendCommand("queryGeometry", payload) ?? false;
|
|
964
|
+
const handleIframeScroll = () => {
|
|
965
|
+
const scrollTop = boundScrollContainer?.scrollTop || 0;
|
|
966
|
+
scrollHandler(scrollTop);
|
|
1358
967
|
};
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
968
|
+
const handleIframeWindowScroll = () => {
|
|
969
|
+
const scrollTop = boundScrollContainer?.scrollTop || 0;
|
|
970
|
+
scrollHandler(scrollTop);
|
|
1362
971
|
};
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
972
|
+
const handleIframeWheel = (event) => {
|
|
973
|
+
if (!event.ctrlKey && !event.metaKey)
|
|
974
|
+
return;
|
|
975
|
+
if (event.deltaY === 0)
|
|
976
|
+
return;
|
|
977
|
+
event.preventDefault();
|
|
978
|
+
event.stopPropagation();
|
|
979
|
+
const zoomContext = (compareView ? slotContext?.primaryPageViewContext : undefined) ??
|
|
980
|
+
pageViewContextRef.current;
|
|
981
|
+
const direction = event.deltaY < 0 ? 1 : -1;
|
|
982
|
+
zoomContext?.setZoom((value) => clampEditorZoom(value + direction * ZOOM_STEP));
|
|
1366
983
|
};
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
984
|
+
const handleIframeKeyDown = (event) => {
|
|
985
|
+
// Handle Ctrl+. keyboard shortcut to open AI text editor
|
|
986
|
+
// Forward this to the parent window since InlineAiTrigger listens there
|
|
987
|
+
const isCtrlPeriod = (event.ctrlKey || event.metaKey) && event.key === ".";
|
|
988
|
+
if (isCtrlPeriod) {
|
|
989
|
+
// Dispatch the inline-ai-open event on the parent window's document
|
|
990
|
+
// so InlineAiTrigger can catch it
|
|
991
|
+
const parentEvent = new CustomEvent("inline-ai-open", {
|
|
992
|
+
bubbles: true,
|
|
993
|
+
cancelable: true,
|
|
994
|
+
});
|
|
995
|
+
window.parent.document.dispatchEvent(parentEvent);
|
|
996
|
+
// Also prevent default to avoid any browser behavior
|
|
997
|
+
event.preventDefault();
|
|
998
|
+
event.stopPropagation();
|
|
999
|
+
return;
|
|
1375
1000
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1001
|
+
editContextRef.current?.handleKeyDown(event);
|
|
1002
|
+
if (editContextRef.current?.mode === "suggestions") {
|
|
1003
|
+
const target = event.target;
|
|
1004
|
+
const fieldElement = target?.closest?.("[data-fieldid][data-itemid][data-language][data-version]");
|
|
1005
|
+
if (fieldElement?.isContentEditable) {
|
|
1006
|
+
setTimeout(() => {
|
|
1007
|
+
const fieldId = fieldElement.getAttribute("data-fieldid");
|
|
1008
|
+
const fieldName = fieldElement.getAttribute("data-fieldname");
|
|
1009
|
+
const itemId = fieldElement.getAttribute("data-itemid");
|
|
1010
|
+
const language = fieldElement.getAttribute("data-language");
|
|
1011
|
+
const versionText = fieldElement.getAttribute("data-version");
|
|
1012
|
+
const version = versionText ? parseInt(versionText, 10) : undefined;
|
|
1013
|
+
if (!fieldId || !itemId || !language || !version)
|
|
1014
|
+
return;
|
|
1015
|
+
const isRichText = fieldElement.getAttribute("data-is-richtext") === "true";
|
|
1016
|
+
const value = (isRichText ? fieldElement.innerHTML : fieldElement.innerText).replaceAll("\u200B", "");
|
|
1017
|
+
editContextRef.current?.operations.editField({
|
|
1018
|
+
field: {
|
|
1019
|
+
fieldId,
|
|
1020
|
+
fieldName: fieldName ?? undefined,
|
|
1021
|
+
item: { id: itemId, language, version },
|
|
1022
|
+
},
|
|
1023
|
+
forceMode: "suggestions",
|
|
1024
|
+
originatingSlotId: slotContext?.slotId,
|
|
1025
|
+
refresh: "none",
|
|
1026
|
+
value,
|
|
1027
|
+
});
|
|
1028
|
+
}, 0);
|
|
1029
|
+
}
|
|
1379
1030
|
}
|
|
1380
1031
|
};
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1032
|
+
const handleIframeBlur = () => {
|
|
1033
|
+
// Block blur event if it was triggered by clicking on a field
|
|
1034
|
+
if (blockBlurEventRef.current < Date.now()) {
|
|
1035
|
+
// Only clear if the focus didn't move to the parent document (e.g. AI dialog)
|
|
1036
|
+
// OR if we still have a selection range.
|
|
1037
|
+
// We use a small timeout because document.activeElement might not be updated yet
|
|
1038
|
+
setTimeout(() => {
|
|
1039
|
+
const activeEl = document.activeElement;
|
|
1040
|
+
const isDialog = activeEl?.closest(".agent-inline-dialog") ||
|
|
1041
|
+
activeEl?.closest('[role="dialog"]');
|
|
1042
|
+
const isTrigger = activeEl?.closest(".agent-inline-trigger");
|
|
1043
|
+
// If focus moved to dialog/trigger (e.g. inline AI), don't force a blur boundary yet.
|
|
1044
|
+
if (!isDialog && !isTrigger) {
|
|
1045
|
+
// Always mark a field blur operation boundary so consecutive edits do not
|
|
1046
|
+
// collapse into a single undo step when a selection range is still present.
|
|
1047
|
+
editContextRef.current?.operations.onFieldBlur?.();
|
|
1048
|
+
// Leaving the iframe ends inline editing even if we keep the
|
|
1049
|
+
// selection metadata. Suggestions mode relies on this blur
|
|
1050
|
+
// boundary so pending suggestions can be re-applied after focus
|
|
1051
|
+
// moves to the surrounding editor UI.
|
|
1052
|
+
fieldsContextRef.current?.setInlineEditingFieldElement(undefined);
|
|
1053
|
+
fieldsContextRef.current?.setFocusedField(undefined, false);
|
|
1054
|
+
}
|
|
1055
|
+
}, 100);
|
|
1387
1056
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
});
|
|
1391
|
-
};
|
|
1392
|
-
pageViewContext.requestBridgeCaptureDom = requestBridgeCaptureDom;
|
|
1393
|
-
return () => {
|
|
1394
|
-
if (pageViewContext.requestBridgeCaptureDom === requestBridgeCaptureDom) {
|
|
1395
|
-
pageViewContext.requestBridgeCaptureDom = undefined;
|
|
1057
|
+
else {
|
|
1058
|
+
blockBlurEventRef.current = 0;
|
|
1396
1059
|
}
|
|
1397
1060
|
};
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
getBridgeGeometryDocumentSize(bridgeGeometry)?.height;
|
|
1407
|
-
const clientHeight = iframe.clientHeight;
|
|
1408
|
-
if (!contentHeight || !clientHeight)
|
|
1409
|
-
return;
|
|
1410
|
-
const upperThreshold = clientHeight + 100; // show minimap if content exceeds this height
|
|
1411
|
-
const lowerThreshold = clientHeight; // hide minimap if content falls below this
|
|
1412
|
-
// Check if minimap is enabled in settings and user controls
|
|
1413
|
-
const minimapEnabled = editContext.parheliaSettings?.showMinimap !== false &&
|
|
1414
|
-
editContext.showMinimap;
|
|
1415
|
-
if (showMiniMap) {
|
|
1416
|
-
if (contentHeight <= lowerThreshold || !minimapEnabled) {
|
|
1417
|
-
setShowMiniMap(false);
|
|
1061
|
+
const handleIframeFocusOut = (event) => {
|
|
1062
|
+
const target = event.target;
|
|
1063
|
+
const fieldElement = target?.closest?.("[data-fieldid][data-itemid],[data-fieldname]");
|
|
1064
|
+
if (!fieldElement)
|
|
1065
|
+
return;
|
|
1066
|
+
const nextFocusedElement = event.relatedTarget;
|
|
1067
|
+
if (nextFocusedElement && fieldElement.contains(nextFocusedElement)) {
|
|
1068
|
+
return;
|
|
1418
1069
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1070
|
+
editContextRef.current?.operations.onFieldBlur?.();
|
|
1071
|
+
fieldsContextRef.current?.setInlineEditingFieldElement(undefined);
|
|
1072
|
+
fieldsContextRef.current?.setFocusedField(undefined, false);
|
|
1073
|
+
};
|
|
1074
|
+
const detachListeners = () => {
|
|
1075
|
+
if (!iframe || !boundDocument || !boundDocumentElement)
|
|
1076
|
+
return;
|
|
1077
|
+
boundDocumentElement.removeEventListener("mousedown", handleIframeMouseDown);
|
|
1078
|
+
boundDocumentElement.removeEventListener("click", handleIframeClick);
|
|
1079
|
+
try {
|
|
1080
|
+
iframe.contentWindow?.removeEventListener("contextmenu", handleContextMenu, true);
|
|
1423
1081
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
!
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
const renderUrl = new URL(urlPath, window.location.origin);
|
|
1454
|
-
alignLoopbackHostToEditor(renderUrl);
|
|
1455
|
-
const editRev = uuid();
|
|
1456
|
-
renderUrl.searchParams.set("edit_rev", editRev);
|
|
1457
|
-
if (editContext.mode !== "preview" && editContext.sessionId) {
|
|
1458
|
-
renderUrl.searchParams.set("parhelia_session", editContext.sessionId);
|
|
1459
|
-
}
|
|
1460
|
-
if (editContext.mode === "preview" && editContext.previewDate) {
|
|
1461
|
-
renderUrl.searchParams.delete("sc_version");
|
|
1462
|
-
renderUrl.searchParams.set("sc_date", toSitecoreDate(editContext.previewDate));
|
|
1463
|
-
}
|
|
1464
|
-
// Layout-mode marker. Only set when editing shared layout, paired with the
|
|
1465
|
-
// `parhelia` editor marker so ParheliaSetLayoutRenderings ignores any unrelated
|
|
1466
|
-
// requests that happen to carry parhelia_layout.
|
|
1467
|
-
if (editContext.mode !== "preview" && editContext.layoutMode === "shared") {
|
|
1468
|
-
renderUrl.searchParams.set("parhelia", "1");
|
|
1469
|
-
renderUrl.searchParams.set("parhelia_layout", "shared");
|
|
1470
|
-
}
|
|
1471
|
-
else {
|
|
1472
|
-
renderUrl.searchParams.delete("parhelia_layout");
|
|
1473
|
-
}
|
|
1474
|
-
// Detect if the version in the URL changed - this requires a full reload, not just requestRefresh
|
|
1475
|
-
// because Next.js router.replace may not properly refetch server data for version changes.
|
|
1476
|
-
const currentIframeUrl = iframeRef.current?.src;
|
|
1477
|
-
const currentIframeOrigin = getUrlOrigin(currentIframeUrl);
|
|
1478
|
-
const renderUrlIsCrossOrigin = renderUrl.origin !== window.location.origin;
|
|
1479
|
-
const iframeOriginChanged = !!(currentIframeOrigin && currentIframeOrigin !== renderUrl.origin);
|
|
1480
|
-
const currentVersion = currentIframeUrl
|
|
1481
|
-
? new URL(currentIframeUrl).searchParams.get("sc_version")
|
|
1482
|
-
: null;
|
|
1483
|
-
const newVersion = renderUrl.searchParams.get("sc_version");
|
|
1484
|
-
const versionChanged = (currentVersion !== null || newVersion !== null) &&
|
|
1485
|
-
currentVersion !== newVersion;
|
|
1486
|
-
const initialLoad = currentItemDescriptor?.id !== pageItemDescriptor.id ||
|
|
1487
|
-
currentItemDescriptor?.language !== pageItemDescriptor.language ||
|
|
1488
|
-
currentItemDescriptor?.version !== pageItemDescriptor.version;
|
|
1489
|
-
const shouldUseIframeSrcReload = initialLoad || pageViewContext.fullscreen || iframeOriginChanged;
|
|
1490
|
-
function runFallbackRefresh() {
|
|
1491
|
-
pageViewContext.setIframeSupportsRefresh(false);
|
|
1492
|
-
setShowSpinner(true);
|
|
1493
|
-
console.log(initialLoad
|
|
1494
|
-
? "Initial load - setting iframe src"
|
|
1495
|
-
: renderUrlIsCrossOrigin
|
|
1496
|
-
? "Cross-origin load - setting iframe src"
|
|
1497
|
-
: iframeOriginChanged
|
|
1498
|
-
? "Iframe origin changed - setting iframe src"
|
|
1499
|
-
: "Reloading iframe src");
|
|
1500
|
-
setIframeSrc(renderUrl.toString());
|
|
1501
|
-
}
|
|
1502
|
-
function runBridgeRefresh() {
|
|
1503
|
-
const bridge = bridgeClientRef.current;
|
|
1504
|
-
if (!bridge?.supportsCapability("refresh")) {
|
|
1505
|
-
runFallbackRefresh();
|
|
1082
|
+
catch { }
|
|
1083
|
+
boundDocument.removeEventListener("selectionchange", selecionChangeHandler);
|
|
1084
|
+
boundDocument.removeEventListener("keydown", handleIframeKeyDown);
|
|
1085
|
+
boundDocument.removeEventListener("wheel", handleIframeWheel, true);
|
|
1086
|
+
boundDocument.removeEventListener("focusout", handleIframeFocusOut, true);
|
|
1087
|
+
boundDocumentElement.removeEventListener("blur", handleIframeBlur, true);
|
|
1088
|
+
boundScrollContainer?.removeEventListener("scroll", handleIframeScroll);
|
|
1089
|
+
try {
|
|
1090
|
+
iframe.contentWindow?.removeEventListener("scroll", handleIframeWindowScroll);
|
|
1091
|
+
}
|
|
1092
|
+
catch { }
|
|
1093
|
+
mutationObserver?.disconnect();
|
|
1094
|
+
mutationObserver = null;
|
|
1095
|
+
boundScrollContainer = null;
|
|
1096
|
+
boundDocumentElement = null;
|
|
1097
|
+
boundDocument = null;
|
|
1098
|
+
};
|
|
1099
|
+
const attachListeners = () => {
|
|
1100
|
+
// The iframe may navigate to a different origin (e.g. an auth redirect),
|
|
1101
|
+
// in which case reading contentDocument/contentWindow.document throws a
|
|
1102
|
+
// SecurityError. Treat that as an inaccessible frame and skip wiring up
|
|
1103
|
+
// listeners instead of letting the exception unwind into React.
|
|
1104
|
+
const iframeDocument = getAccessibleIframeDocument(iframe, "PageViewerFrame.tsx:attachListeners-document-access");
|
|
1105
|
+
const iframeDocumentElement = iframeDocument?.documentElement;
|
|
1106
|
+
if (!iframeDocument || !iframeDocumentElement)
|
|
1107
|
+
return;
|
|
1108
|
+
// Skip if already bound to current root element.
|
|
1109
|
+
if (boundDocument === iframeDocument &&
|
|
1110
|
+
boundDocumentElement === iframeDocumentElement) {
|
|
1506
1111
|
return;
|
|
1507
1112
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1113
|
+
detachListeners();
|
|
1114
|
+
boundDocument = iframeDocument;
|
|
1115
|
+
boundDocumentElement = iframeDocumentElement;
|
|
1116
|
+
iframeDocumentElement.addEventListener("mousedown", handleIframeMouseDown);
|
|
1117
|
+
iframeDocumentElement.addEventListener("click", handleIframeClick);
|
|
1118
|
+
try {
|
|
1119
|
+
iframe.contentWindow?.addEventListener("contextmenu", handleContextMenu, true);
|
|
1120
|
+
}
|
|
1121
|
+
catch { }
|
|
1122
|
+
boundScrollContainer =
|
|
1123
|
+
iframeDocument.scrollingElement || iframeDocument.body || null;
|
|
1124
|
+
boundScrollContainer?.addEventListener("scroll", handleIframeScroll);
|
|
1125
|
+
try {
|
|
1126
|
+
iframe.contentWindow?.addEventListener("scroll", handleIframeWindowScroll);
|
|
1127
|
+
}
|
|
1128
|
+
catch { }
|
|
1129
|
+
iframeDocument.addEventListener("selectionchange", selecionChangeHandler);
|
|
1130
|
+
iframeDocument.addEventListener("keydown", handleIframeKeyDown);
|
|
1131
|
+
iframeDocument.addEventListener("wheel", handleIframeWheel, {
|
|
1132
|
+
capture: true,
|
|
1133
|
+
passive: false,
|
|
1514
1134
|
});
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1135
|
+
iframeDocument.addEventListener("focusout", handleIframeFocusOut, true);
|
|
1136
|
+
iframeDocumentElement.addEventListener("blur", handleIframeBlur, true);
|
|
1137
|
+
mutationObserver = new MutationObserver((records) => {
|
|
1138
|
+
// Ignore all text field edits
|
|
1139
|
+
if (records.some((x) => !(x &&
|
|
1140
|
+
"getAttribute" in x.target &&
|
|
1141
|
+
x.target.getAttribute("data-fieldid") &&
|
|
1142
|
+
x.target.getAttribute("data-itemid")))) {
|
|
1143
|
+
requestPageModelBuild(iframeDocument);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
mutationObserver.observe(iframeDocument, {
|
|
1147
|
+
childList: true, // observe direct children changes
|
|
1148
|
+
subtree: true, // observe all descendants changes
|
|
1149
|
+
characterData: false, // observe text changes
|
|
1150
|
+
//attributes: true, // observe attribute changes (like style or class)
|
|
1151
|
+
});
|
|
1152
|
+
requestPageModelBuild(iframeDocument);
|
|
1153
|
+
};
|
|
1154
|
+
const parsePositiveInt = (value) => {
|
|
1155
|
+
if (!value)
|
|
1156
|
+
return undefined;
|
|
1157
|
+
const parsed = parseInt(value, 10);
|
|
1158
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1159
|
+
};
|
|
1160
|
+
const resolveLinkedItemDescriptor = (anchor, clickedElement) => {
|
|
1161
|
+
const href = anchor.getAttribute("href") || anchor.href;
|
|
1162
|
+
let hrefUrl;
|
|
1163
|
+
try {
|
|
1164
|
+
hrefUrl = new URL(anchor.href, getAccessibleIframeLocationHref(iframe) || window.location.href);
|
|
1518
1165
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (bridgeClientRef.current?.supportsCapability("refresh")) {
|
|
1522
|
-
runBridgeRefresh();
|
|
1166
|
+
catch {
|
|
1167
|
+
return undefined;
|
|
1523
1168
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1169
|
+
const attributeContainer = clickedElement?.closest("[data-itemid],[data-language],[data-version],[sc_item],[sc_itemid],[sc_lang]") || anchor;
|
|
1170
|
+
// Parse Sitecore RTE format first: ~/link.aspx?_id=GUID&_z=z (target item, not the containing component)
|
|
1171
|
+
let itemIdRaw;
|
|
1172
|
+
if (href.includes("~/link.aspx?_id=") ||
|
|
1173
|
+
href.includes("link.aspx?_id=")) {
|
|
1174
|
+
const idFromUrl = hrefUrl.searchParams.get("_id");
|
|
1175
|
+
if (idFromUrl) {
|
|
1176
|
+
// Convert compact GUID (AD973E51E8454BD2B333859375FBBA24) to standard format with dashes
|
|
1177
|
+
itemIdRaw = idFromUrl
|
|
1178
|
+
.replace(/^(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})$/i, "$1-$2-$3-$4-$5")
|
|
1179
|
+
.toLowerCase();
|
|
1180
|
+
}
|
|
1531
1181
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1182
|
+
if (!itemIdRaw) {
|
|
1183
|
+
itemIdRaw =
|
|
1184
|
+
hrefUrl.searchParams.get("sc_itemid") ??
|
|
1185
|
+
hrefUrl.searchParams.get("itemid") ??
|
|
1186
|
+
anchor.getAttribute("data-itemid") ??
|
|
1187
|
+
anchor.getAttribute("sc_itemid") ??
|
|
1188
|
+
attributeContainer?.getAttribute("data-itemid") ??
|
|
1189
|
+
attributeContainer?.getAttribute("sc_itemid") ??
|
|
1190
|
+
(anchor.getAttribute("sc_item")
|
|
1191
|
+
? extractItemIdFromItemUri(anchor.getAttribute("sc_item"))
|
|
1192
|
+
: undefined) ??
|
|
1193
|
+
(attributeContainer?.getAttribute("sc_item")
|
|
1194
|
+
? extractItemIdFromItemUri(attributeContainer.getAttribute("sc_item"))
|
|
1195
|
+
: undefined);
|
|
1196
|
+
}
|
|
1197
|
+
const itemId = cleanId(itemIdRaw);
|
|
1198
|
+
if (!itemId)
|
|
1199
|
+
return undefined;
|
|
1200
|
+
const language = hrefUrl.searchParams.get("sc_lang") ??
|
|
1201
|
+
hrefUrl.searchParams.get("lang") ??
|
|
1202
|
+
hrefUrl.searchParams.get("language") ??
|
|
1203
|
+
anchor.getAttribute("data-language") ??
|
|
1204
|
+
anchor.getAttribute("sc_lang") ??
|
|
1205
|
+
attributeContainer?.getAttribute("data-language") ??
|
|
1206
|
+
attributeContainer?.getAttribute("sc_lang") ??
|
|
1207
|
+
editContextRef.current?.currentItemDescriptor?.language ??
|
|
1208
|
+
editContextRef.current?.item?.language;
|
|
1209
|
+
if (!language)
|
|
1210
|
+
return undefined;
|
|
1211
|
+
const version = parsePositiveInt(hrefUrl.searchParams.get("sc_version")) ??
|
|
1212
|
+
parsePositiveInt(hrefUrl.searchParams.get("version")) ??
|
|
1213
|
+
parsePositiveInt(anchor.getAttribute("data-version")) ??
|
|
1214
|
+
parsePositiveInt(attributeContainer?.getAttribute("data-version")) ??
|
|
1215
|
+
editContextRef.current?.currentItemDescriptor?.version ??
|
|
1216
|
+
editContextRef.current?.item?.version ??
|
|
1217
|
+
0;
|
|
1218
|
+
return {
|
|
1219
|
+
id: itemId,
|
|
1220
|
+
language,
|
|
1221
|
+
version,
|
|
1222
|
+
};
|
|
1223
|
+
};
|
|
1224
|
+
const handleIframeClick = async (event) => {
|
|
1225
|
+
const target = event.target;
|
|
1226
|
+
if (!target)
|
|
1227
|
+
return;
|
|
1228
|
+
const anchor = target.tagName.toLowerCase() === "a"
|
|
1229
|
+
? target
|
|
1230
|
+
: target.closest("a");
|
|
1231
|
+
if (!anchor)
|
|
1232
|
+
return;
|
|
1233
|
+
const href = anchor.getAttribute("href") || anchor.href;
|
|
1234
|
+
if (!href || href.startsWith("#") || href.startsWith("javascript:")) {
|
|
1553
1235
|
return;
|
|
1554
|
-
const bounds = findBridgeFieldDocumentBounds(pageViewContextRef.current?.bridgeGeometry, fieldsContext.focusedField);
|
|
1555
|
-
const activePageViewContext = pageViewContextRef.current;
|
|
1556
|
-
if (bounds && activePageViewContext) {
|
|
1557
|
-
scrollBridgeBoundsIntoView(activePageViewContext, bounds, latestBridgeScrollRef.current);
|
|
1558
1236
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1237
|
+
const mode = editContextRef.current?.mode;
|
|
1238
|
+
const isPreviewOrSuggestions = mode === "preview" || mode === "suggestions";
|
|
1239
|
+
if (isPreviewOrSuggestions) {
|
|
1240
|
+
let hrefUrl;
|
|
1241
|
+
try {
|
|
1242
|
+
hrefUrl = new URL(anchor.href, getAccessibleIframeLocationHref(iframe) || window.location.href);
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const iframeOrigin = getAccessibleIframeLocationOrigin(iframe);
|
|
1248
|
+
const isInternalLink = hrefUrl.origin === window.location.origin ||
|
|
1249
|
+
(iframeOrigin ? hrefUrl.origin === iframeOrigin : false);
|
|
1250
|
+
if (!isInternalLink) {
|
|
1251
|
+
event.preventDefault();
|
|
1252
|
+
event.stopPropagation();
|
|
1253
|
+
window.open(hrefUrl.toString(), "_blank", "noopener,noreferrer");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (isInternalLink) {
|
|
1257
|
+
const linkedItem = resolveLinkedItemDescriptor(anchor, target);
|
|
1258
|
+
if (linkedItem) {
|
|
1259
|
+
event.preventDefault();
|
|
1260
|
+
event.stopPropagation();
|
|
1261
|
+
await editContextRef.current?.loadItem(linkedItem, {
|
|
1262
|
+
openInNewSlot: event.altKey,
|
|
1263
|
+
});
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
// If this link cannot be resolved to an item, allow browser behavior.
|
|
1268
|
+
return;
|
|
1579
1269
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
const activePageViewContext = pageViewContextRef.current;
|
|
1586
|
-
if (!activePageViewContext)
|
|
1587
|
-
return;
|
|
1588
|
-
const bounds = findBridgeComponentDocumentBounds(activePageViewContext.bridgeGeometry, editContext.scrollIntoView);
|
|
1589
|
-
if (bounds) {
|
|
1590
|
-
scrollBridgeBoundsIntoView(activePageViewContext, bounds, latestBridgeScrollRef.current);
|
|
1591
|
-
}
|
|
1592
|
-
editContext.setScrollIntoView(undefined);
|
|
1593
|
-
}, [editContext.scrollIntoView, pageViewContext.page]);
|
|
1594
|
-
useEffect(() => {
|
|
1595
|
-
const handleMessage = (message) => {
|
|
1596
|
-
if (message.origin !== window.location.origin)
|
|
1270
|
+
// In edit mode, keep navigation inside iframe disabled.
|
|
1271
|
+
event.preventDefault();
|
|
1272
|
+
};
|
|
1273
|
+
const handleContextMenu = async (event) => {
|
|
1274
|
+
if (editContextRef.current?.isRefreshing && showSpinner)
|
|
1597
1275
|
return;
|
|
1598
|
-
|
|
1599
|
-
|
|
1276
|
+
const target = event.target;
|
|
1277
|
+
if (!target)
|
|
1278
|
+
return;
|
|
1279
|
+
if (event.ctrlKey)
|
|
1280
|
+
return;
|
|
1281
|
+
event.preventDefault();
|
|
1282
|
+
event.stopPropagation();
|
|
1283
|
+
let componentId = findNearestEditableComponentId(target);
|
|
1284
|
+
const pageForContextMenu = pageViewContextRef.current?.page;
|
|
1285
|
+
if (componentId && pageForContextMenu) {
|
|
1286
|
+
componentId = resolveComponentIdForTarget(componentId, target, pageForContextMenu);
|
|
1600
1287
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1288
|
+
// Layout components can now be right-clicked even when showLayoutComponents is false
|
|
1289
|
+
// Context menu will show limited/read-only actions
|
|
1290
|
+
if (componentId) {
|
|
1291
|
+
// Only change selection if right-clicking on a component that's not in the current selection
|
|
1292
|
+
if (!editContextRef.current?.selection.includes(componentId)) {
|
|
1293
|
+
// Right-clicking on a component not in the selection - select just that component
|
|
1294
|
+
editContextRef.current.select([componentId]);
|
|
1295
|
+
}
|
|
1296
|
+
// else: right-clicking on a component already in the selection - keep current selection
|
|
1603
1297
|
}
|
|
1298
|
+
const fieldElement = findParentWithAttribute(target, "data-fieldid");
|
|
1299
|
+
const selectedComponents = editContextRef.current?.selection
|
|
1300
|
+
.map((id) => getComponentById(id, pageViewContextRef.current.page))
|
|
1301
|
+
.filter((x) => x);
|
|
1302
|
+
// Context menu will now show for layout components even when showLayoutComponents is false
|
|
1303
|
+
// Commands will be appropriately filtered/disabled
|
|
1304
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
1305
|
+
const adjustedEvent = new MouseEvent("contextmenu", {
|
|
1306
|
+
bubbles: true,
|
|
1307
|
+
cancelable: true,
|
|
1308
|
+
shiftKey: event.shiftKey,
|
|
1309
|
+
altKey: event.altKey,
|
|
1310
|
+
ctrlKey: event.ctrlKey,
|
|
1311
|
+
view: window,
|
|
1312
|
+
clientX: event.clientX + iframeRect.x,
|
|
1313
|
+
clientY: event.clientY + iframeRect.y,
|
|
1314
|
+
});
|
|
1315
|
+
// Store the original context menu position for overlay positioning
|
|
1316
|
+
const menuPosition = {
|
|
1317
|
+
x: event.clientX + iframeRect.x,
|
|
1318
|
+
y: event.clientY + iframeRect.y,
|
|
1319
|
+
};
|
|
1320
|
+
setContextMenuPosition(menuPosition);
|
|
1321
|
+
// Only show a spinner if work takes longer than 100ms
|
|
1322
|
+
let loadingShown = false;
|
|
1323
|
+
const loadingTimer = setTimeout(() => {
|
|
1324
|
+
loadingShown = true;
|
|
1325
|
+
editContextRef.current?.showContextMenu(adjustedEvent, [
|
|
1326
|
+
{
|
|
1327
|
+
id: "loading",
|
|
1328
|
+
label: "Loading…",
|
|
1329
|
+
disabled: true,
|
|
1330
|
+
icon: _jsx(Spinner, { size: "sm", className: "mr-2" }),
|
|
1331
|
+
},
|
|
1332
|
+
]);
|
|
1333
|
+
}, 100);
|
|
1334
|
+
const field = fieldElement
|
|
1335
|
+
? getFieldDescriptorFromElement(fieldElement)
|
|
1336
|
+
: undefined;
|
|
1337
|
+
const fieldButtons = field ? await loadFieldButtons(field) : [];
|
|
1338
|
+
// Create a handler that has access to the current menu position
|
|
1339
|
+
const handleParameterizedActionWithPosition = (field, action, allFieldButtons, event) => {
|
|
1340
|
+
setContextMenuField(field);
|
|
1341
|
+
setContextMenuFieldButtons([action]);
|
|
1342
|
+
setPreSelectedAction(action);
|
|
1343
|
+
// Create a new MouseEvent with the captured position
|
|
1344
|
+
const syntheticEvent = new MouseEvent("click", {
|
|
1345
|
+
bubbles: true,
|
|
1346
|
+
cancelable: true,
|
|
1347
|
+
view: window,
|
|
1348
|
+
clientX: menuPosition.x,
|
|
1349
|
+
clientY: menuPosition.y,
|
|
1350
|
+
screenX: menuPosition.x,
|
|
1351
|
+
screenY: menuPosition.y,
|
|
1352
|
+
});
|
|
1353
|
+
// Add target property to the event for proper positioning
|
|
1354
|
+
Object.defineProperty(syntheticEvent, "target", {
|
|
1355
|
+
value: document.body,
|
|
1356
|
+
enumerable: true,
|
|
1357
|
+
});
|
|
1358
|
+
console.log("About to show overlay with position:", {
|
|
1359
|
+
menuPosition,
|
|
1360
|
+
syntheticEvent,
|
|
1361
|
+
preSelectedAction: action,
|
|
1362
|
+
});
|
|
1363
|
+
// Add a small delay to ensure context menu has closed
|
|
1364
|
+
setTimeout(() => {
|
|
1365
|
+
console.log("Showing overlay with position:", {
|
|
1366
|
+
menuPosition,
|
|
1367
|
+
syntheticEvent,
|
|
1368
|
+
preSelectedAction: action,
|
|
1369
|
+
});
|
|
1370
|
+
// Create a temporary element at the exact position for better positioning
|
|
1371
|
+
const tempElement = document.createElement("div");
|
|
1372
|
+
tempElement.style.position = "fixed";
|
|
1373
|
+
tempElement.style.left = menuPosition.x + "px";
|
|
1374
|
+
tempElement.style.top = menuPosition.y + "px";
|
|
1375
|
+
tempElement.style.width = "1px";
|
|
1376
|
+
tempElement.style.height = "1px";
|
|
1377
|
+
tempElement.style.visibility = "hidden";
|
|
1378
|
+
tempElement.style.pointerEvents = "none";
|
|
1379
|
+
document.body.appendChild(tempElement);
|
|
1380
|
+
// Create event targeting the positioned element
|
|
1381
|
+
const positionedEvent = new MouseEvent("click", {
|
|
1382
|
+
bubbles: true,
|
|
1383
|
+
cancelable: true,
|
|
1384
|
+
view: window,
|
|
1385
|
+
clientX: menuPosition.x,
|
|
1386
|
+
clientY: menuPosition.y,
|
|
1387
|
+
});
|
|
1388
|
+
Object.defineProperty(positionedEvent, "target", {
|
|
1389
|
+
value: tempElement,
|
|
1390
|
+
enumerable: true,
|
|
1391
|
+
});
|
|
1392
|
+
fieldActionsOverlay.current?.show(positionedEvent, action);
|
|
1393
|
+
// Clean up the temporary element after overlay is shown
|
|
1394
|
+
setTimeout(() => {
|
|
1395
|
+
document.body.removeChild(tempElement);
|
|
1396
|
+
}, 1000);
|
|
1397
|
+
}, 100);
|
|
1398
|
+
};
|
|
1399
|
+
const items = await buildComponentContextMenuItems(selectedComponents, editContextRef.current, field, fieldButtons, handleParameterizedActionWithPosition);
|
|
1400
|
+
clearTimeout(loadingTimer);
|
|
1401
|
+
if (loadingShown)
|
|
1402
|
+
editContextRef.current?.updateContextMenu(items);
|
|
1403
|
+
else
|
|
1404
|
+
editContextRef.current?.showContextMenu(adjustedEvent, items);
|
|
1604
1405
|
};
|
|
1605
|
-
window.addEventListener("message", handleMessage);
|
|
1606
|
-
return () => {
|
|
1607
|
-
window.removeEventListener("message", handleMessage);
|
|
1608
|
-
};
|
|
1609
|
-
}, []);
|
|
1610
|
-
useEffect(() => {
|
|
1611
|
-
const iframe = iframeRef.current;
|
|
1612
|
-
if (!iframe)
|
|
1613
|
-
return;
|
|
1614
1406
|
const handleLoad = () => {
|
|
1407
|
+
// Skip handling if this load does not correspond to the latest requested revision
|
|
1408
|
+
const expectedRevision = currentLoadRef.current?.revision ?? "";
|
|
1409
|
+
const currentRevision = editContextRef.current?.revision ?? "";
|
|
1410
|
+
if (expectedRevision && expectedRevision !== currentRevision) {
|
|
1411
|
+
console.log("Stale load skipped", {
|
|
1412
|
+
expectedRevision,
|
|
1413
|
+
currentRevision,
|
|
1414
|
+
});
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1615
1417
|
setShowSpinner(false);
|
|
1616
1418
|
applyIframeZoom(iframe, pageViewContextRef.current?.zoom ?? 1, "PageViewerFrame.tsx:handleLoad-zoom");
|
|
1419
|
+
attachListeners();
|
|
1617
1420
|
};
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
iframe
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (!iframe || typeof ResizeObserver === "undefined")
|
|
1626
|
-
return;
|
|
1627
|
-
let lastWidth = iframe.clientWidth;
|
|
1628
|
-
let lastHeight = iframe.clientHeight;
|
|
1629
|
-
let geometryTimer = null;
|
|
1630
|
-
let trailingGeometryTimer = null;
|
|
1631
|
-
const requestGeometry = () => {
|
|
1632
|
-
geometryTimer = null;
|
|
1633
|
-
const nextWidth = iframe.clientWidth;
|
|
1634
|
-
const nextHeight = iframe.clientHeight;
|
|
1635
|
-
if (nextWidth === lastWidth && nextHeight === lastHeight)
|
|
1636
|
-
return;
|
|
1637
|
-
lastWidth = nextWidth;
|
|
1638
|
-
lastHeight = nextHeight;
|
|
1639
|
-
bridgeClientRef.current?.sendCommand("queryGeometry", {});
|
|
1640
|
-
};
|
|
1641
|
-
const scheduleGeometryRequest = () => {
|
|
1642
|
-
if (geometryTimer != null) {
|
|
1643
|
-
window.clearTimeout(geometryTimer);
|
|
1421
|
+
rebindIframeInteractionsRef.current = attachListeners;
|
|
1422
|
+
if (iframe) {
|
|
1423
|
+
// If the iframe is already loaded, attach the listener immediately
|
|
1424
|
+
const iframeDoc = getAccessibleIframeDocument(iframe, "PageViewerFrame.tsx:initial-attach-ready-state");
|
|
1425
|
+
if (iframeDoc?.readyState === "complete" ||
|
|
1426
|
+
iframeDoc?.readyState === "interactive") {
|
|
1427
|
+
handleLoad();
|
|
1644
1428
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
geometryTimer = window.setTimeout(requestGeometry, 50);
|
|
1649
|
-
trailingGeometryTimer = window.setTimeout(() => {
|
|
1650
|
-
trailingGeometryTimer = null;
|
|
1651
|
-
requestGeometry();
|
|
1652
|
-
}, ZOOM_TRANSITION_MS + 75);
|
|
1653
|
-
};
|
|
1654
|
-
const resizeObserver = new ResizeObserver(scheduleGeometryRequest);
|
|
1655
|
-
resizeObserver.observe(iframe);
|
|
1429
|
+
iframe.addEventListener("load", handleLoad);
|
|
1430
|
+
}
|
|
1431
|
+
// Cleanup function
|
|
1656
1432
|
return () => {
|
|
1657
|
-
|
|
1658
|
-
if (
|
|
1659
|
-
|
|
1660
|
-
}
|
|
1661
|
-
if (trailingGeometryTimer != null) {
|
|
1662
|
-
window.clearTimeout(trailingGeometryTimer);
|
|
1433
|
+
rebindIframeInteractionsRef.current = null;
|
|
1434
|
+
if (iframe) {
|
|
1435
|
+
iframe.removeEventListener("load", handleLoad);
|
|
1663
1436
|
}
|
|
1437
|
+
detachListeners();
|
|
1664
1438
|
};
|
|
1665
|
-
}, [
|
|
1439
|
+
}, [iframeRef.current]);
|
|
1666
1440
|
useEffect(() => {
|
|
1667
|
-
|
|
1668
|
-
componentIds: editContext.selection,
|
|
1669
|
-
}
|
|
1441
|
+
try {
|
|
1442
|
+
iframeRef.current?.contentWindow?.postMessage({ type: "componentsSelected", componentIds: editContext.selection }, window.location.origin);
|
|
1443
|
+
}
|
|
1444
|
+
catch { }
|
|
1670
1445
|
}, [editContext.selection]);
|
|
1671
|
-
|
|
1672
|
-
const
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
layoutKind: editContext.layoutMode,
|
|
1683
|
-
});
|
|
1684
|
-
}, [editContext.layoutMode]);
|
|
1446
|
+
const updateScrollPosition = (e) => {
|
|
1447
|
+
const shouldTrackMinimapScroll = showMiniMap &&
|
|
1448
|
+
editContextRef.current?.showMinimap &&
|
|
1449
|
+
!editContextRef.current?.isMobile &&
|
|
1450
|
+
editContextRef.current?.parheliaSettings?.showMinimap !== false;
|
|
1451
|
+
if (!shouldTrackMinimapScroll)
|
|
1452
|
+
return;
|
|
1453
|
+
setScroll(e);
|
|
1454
|
+
if (!compareView)
|
|
1455
|
+
pageViewContextRef.current?.setScroll(e);
|
|
1456
|
+
};
|
|
1685
1457
|
useEffect(() => {
|
|
1686
1458
|
applyIframeZoom(iframeRef.current, zoom, "PageViewerFrame.tsx:zoom-doc");
|
|
1687
|
-
bridgeClientRef.current?.sendCommand("setZoom", { zoom });
|
|
1688
|
-
const geometryTimer = window.setTimeout(() => {
|
|
1689
|
-
bridgeClientRef.current?.sendCommand("queryGeometry", {});
|
|
1690
|
-
}, ZOOM_TRANSITION_MS + 75);
|
|
1691
|
-
return () => {
|
|
1692
|
-
window.clearTimeout(geometryTimer);
|
|
1693
|
-
};
|
|
1694
|
-
}, [zoom]);
|
|
1695
|
-
useEffect(() => {
|
|
1696
|
-
let geometryTimer = null;
|
|
1697
|
-
const sendViewportStateToBridge = (event) => {
|
|
1698
|
-
const detail = event.detail;
|
|
1699
|
-
const nextZoom = detail?.zoom ?? pageViewContextRef.current?.zoom ?? zoom;
|
|
1700
|
-
bridgeClientRef.current?.sendCommand("setZoom", {
|
|
1701
|
-
zoom: nextZoom,
|
|
1702
|
-
});
|
|
1703
|
-
if (geometryTimer != null) {
|
|
1704
|
-
window.clearTimeout(geometryTimer);
|
|
1705
|
-
}
|
|
1706
|
-
geometryTimer = window.setTimeout(() => {
|
|
1707
|
-
geometryTimer = null;
|
|
1708
|
-
bridgeClientRef.current?.sendCommand("queryGeometry", {});
|
|
1709
|
-
}, ZOOM_TRANSITION_MS + 75);
|
|
1710
|
-
};
|
|
1711
|
-
window.addEventListener(DEVICE_CHANGE_EVENT, sendViewportStateToBridge);
|
|
1712
|
-
return () => {
|
|
1713
|
-
if (geometryTimer != null) {
|
|
1714
|
-
window.clearTimeout(geometryTimer);
|
|
1715
|
-
}
|
|
1716
|
-
window.removeEventListener(DEVICE_CHANGE_EVENT, sendViewportStateToBridge);
|
|
1717
|
-
};
|
|
1718
1459
|
}, [zoom]);
|
|
1460
|
+
const scrollHandler = useThrottledCallback(updateScrollPosition, 100);
|
|
1719
1461
|
if (pageViewContext.page?.item && !pageViewContext.page?.item.hasLayout) {
|
|
1720
1462
|
return _jsx(NoLayout, {});
|
|
1721
1463
|
}
|
|
@@ -1731,23 +1473,265 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
1731
1473
|
"px";
|
|
1732
1474
|
return (_jsxs("div", { className: cn("relative flex h-full w-full flex-col items-center select-none", className, editContext.showAgentsPanel && !editContext.currentWizardId && "pr-0"), children: [!pageViewContext.fullscreen && (_jsx(EditorWarnings, { item: pageViewContext.page?.item })), slotCloseButton && (_jsx("div", { className: "absolute top-3 right-3 z-50", children: slotCloseButton })), pageViewContext.device !== "desktop" && (_jsx(DeviceToolbar, { pageViewContext: pageViewContext, configuration: editContext.configuration })), _jsxs("div", { className: "relative flex flex-1 transition-[width] duration-300 ease-in-out select-none motion-reduce:transition-none", "data-testid": "page-viewer-viewport", style: {
|
|
1733
1475
|
width: deviceWidth,
|
|
1734
|
-
}, children: [_jsxs("div", { className: "relative h-full w-full overflow-hidden", children: [_jsx("iframe", { ref:
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
});
|
|
1739
|
-
};
|
|
1740
|
-
sendZoomToBridge();
|
|
1741
|
-
window.setTimeout(sendZoomToBridge, 100);
|
|
1742
|
-
window.setTimeout(sendZoomToBridge, 500);
|
|
1743
|
-
if (iframeSrc) {
|
|
1476
|
+
}, children: [_jsxs("div", { className: "relative h-full w-full overflow-hidden", children: [_jsx("iframe", { ref: iframeRef, className: "page-iframe h-full w-full bg-white transition-[height] duration-300 ease-in-out motion-reduce:transition-none", style: { height: deviceHeight }, src: iframeSrc, "data-testid": "pageEditoriframe", onLoad: () => {
|
|
1477
|
+
// Handle iframe load when using src attribute (initial load)
|
|
1478
|
+
const doc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:onLoad-doc");
|
|
1479
|
+
if (doc && iframeSrc) {
|
|
1744
1480
|
setShowSpinner(false);
|
|
1481
|
+
ensureEditorCssGuard(doc, editContext.mode !== "preview");
|
|
1745
1482
|
applyIframeZoom(iframeRef.current, zoom, "PageViewerFrame.tsx:onLoad-zoom");
|
|
1483
|
+
setTimeout(() => {
|
|
1484
|
+
injectSXAScripts(iframeRef.current);
|
|
1485
|
+
}, 1000);
|
|
1486
|
+
requestPageModelBuild(doc);
|
|
1746
1487
|
}
|
|
1747
|
-
} }),
|
|
1488
|
+
} }), iframeRef.current && (_jsx(IframeOverlayProvider, { iframe: iframeRef.current, mode: "scrolling", scrollScale: 1, visualScale: 1, children: _jsx(PageEditorChrome, { iframe: iframeRef.current, compareView: compareView, pageViewContext: pageViewContext }) })), pageViewContext.deviceHeight && pageViewContext.device && (_jsx("div", { className: "bg-neutral-grey-5 relative z-40 h-full w-full" }))] }), !pageViewContext.fullscreen &&
|
|
1748
1489
|
showMiniMap &&
|
|
1749
1490
|
editContext.showMinimap &&
|
|
1750
1491
|
!editContext.isMobile &&
|
|
1751
|
-
editContext.parheliaSettings?.showMinimap !== false && (_jsx(MiniMap, { compareView: compareView, scroll: scroll, mainViewIframeRef: iframeRef, pageViewContext: pageViewContext
|
|
1492
|
+
editContext.parheliaSettings?.showMinimap !== false && (_jsx(MiniMap, { compareView: compareView, scroll: scroll, mainViewIframeRef: iframeRef, pageViewContext: pageViewContext, deviceHeight: pageViewContext.device === "desktop"
|
|
1493
|
+
? undefined
|
|
1494
|
+
: pageViewContext.deviceHeight })), showSpinner && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bg-neutral-grey-5/50 absolute top-0 left-0 h-full w-full" }), _jsx("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform", children: _jsx(Spinner, {}) })] }))] }), _jsx(FieldActionsOverlay, { ref: fieldActionsOverlay, generatorButtons: contextMenuFieldButtons, onActionClick: handleActionClick, onParameterizedActionExecute: handleParameterizedActionExecute, currentOverlay: editContext?.currentOverlay, fieldId: contextMenuField?.fieldId || "", setCurrentOverlay: editContext?.setCurrentOverlay || (() => { }), preSelectedAction: preSelectedAction, iframe: iframeRef.current })] }));
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* In preview mode, disable editing in the iframe: set contentEditable=false on all
|
|
1498
|
+
* editable elements and blur the active element so the caret is removed and typing does nothing.
|
|
1499
|
+
*/
|
|
1500
|
+
function disableEditingInIframeDocument(doc) {
|
|
1501
|
+
if (!doc || !doc.body)
|
|
1502
|
+
return;
|
|
1503
|
+
const editable = doc.querySelectorAll("[contenteditable='true'], [contenteditable='']");
|
|
1504
|
+
editable.forEach((el) => {
|
|
1505
|
+
el.contentEditable = "false";
|
|
1506
|
+
});
|
|
1507
|
+
const active = doc.activeElement;
|
|
1508
|
+
if (active?.isContentEditable) {
|
|
1509
|
+
active.blur();
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function injectEditorCSS(iframeDocument, editMode) {
|
|
1513
|
+
if (!iframeDocument)
|
|
1514
|
+
return;
|
|
1515
|
+
const styleId = EDITOR_CSS_STYLE_ID;
|
|
1516
|
+
// Remove existing style element if present to avoid duplicates
|
|
1517
|
+
const existingStyle = iframeDocument.getElementById(styleId);
|
|
1518
|
+
if (existingStyle) {
|
|
1519
|
+
existingStyle.remove();
|
|
1520
|
+
}
|
|
1521
|
+
const style = iframeDocument.createElement("style");
|
|
1522
|
+
style.id = styleId;
|
|
1523
|
+
style.textContent = editMode
|
|
1524
|
+
? `[contentEditable]:empty:before {
|
|
1525
|
+
content: attr(placeholder);
|
|
1526
|
+
opacity: 0.6;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.scChromeData, code {
|
|
1530
|
+
display: none;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
[contenteditable] {
|
|
1534
|
+
outline: 0px solid transparent;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
[data-fieldid][data-itemid][data-language][data-version][data-is-richtext="true"] {
|
|
1538
|
+
cursor: text;
|
|
1539
|
+
}
|
|
1540
|
+
`
|
|
1541
|
+
: `
|
|
1542
|
+
[contenteditable] {
|
|
1543
|
+
outline: 0px solid transparent;
|
|
1544
|
+
}
|
|
1545
|
+
.scChromeData, code {
|
|
1546
|
+
display: none;
|
|
1547
|
+
}
|
|
1548
|
+
`;
|
|
1549
|
+
if (iframeDocument && iframeDocument.head) {
|
|
1550
|
+
iframeDocument.head.appendChild(style);
|
|
1551
|
+
}
|
|
1552
|
+
if (!editMode) {
|
|
1553
|
+
disableEditingInIframeDocument(iframeDocument);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function resolveComponentIdForTarget(rawId, target, page) {
|
|
1557
|
+
const direct = getComponentById(rawId, page);
|
|
1558
|
+
const candidates = getAllComponentInstances(rawId, page);
|
|
1559
|
+
if (candidates.length === 0) {
|
|
1560
|
+
return direct?.id || rawId;
|
|
1561
|
+
}
|
|
1562
|
+
const containing = candidates.filter((component) => isTargetInsideComponent(target, component));
|
|
1563
|
+
if (containing.length === 0) {
|
|
1564
|
+
return direct?.id || candidates[0].id;
|
|
1565
|
+
}
|
|
1566
|
+
// Prefer the deepest/most specific matching component for nested SXA markup.
|
|
1567
|
+
containing.sort((a, b) => getElementDepth(b.firstDOMElement || null) -
|
|
1568
|
+
getElementDepth(a.firstDOMElement || null));
|
|
1569
|
+
return containing[0].id;
|
|
1570
|
+
}
|
|
1571
|
+
function resolveComponentIdForFieldTarget(field, target, page) {
|
|
1572
|
+
const matches = findComponentsRenderingField(field, page);
|
|
1573
|
+
const containingMatches = matches.filter((component) => isTargetInsideComponent(target, component));
|
|
1574
|
+
const rankedMatches = (containingMatches.length > 0 ? containingMatches : matches).sort((a, b) => getElementDepth(b.firstDOMElement || null) -
|
|
1575
|
+
getElementDepth(a.firstDOMElement || null));
|
|
1576
|
+
return rankedMatches[0]?.id;
|
|
1577
|
+
}
|
|
1578
|
+
function findComponentsRenderingField(field, page) {
|
|
1579
|
+
if (!page?.rootComponent)
|
|
1580
|
+
return [];
|
|
1581
|
+
const matches = [];
|
|
1582
|
+
const visit = (component) => {
|
|
1583
|
+
if (componentRendersField(component, field)) {
|
|
1584
|
+
matches.push(component);
|
|
1585
|
+
}
|
|
1586
|
+
for (const placeholder of component.placeholders || []) {
|
|
1587
|
+
for (const child of placeholder.components || []) {
|
|
1588
|
+
visit(child);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
visit(page.rootComponent);
|
|
1593
|
+
return matches;
|
|
1594
|
+
}
|
|
1595
|
+
function componentRendersField(component, field) {
|
|
1596
|
+
const renderedItems = [
|
|
1597
|
+
component.datasourceItem,
|
|
1598
|
+
...component.items.filter((item) => !component.datasourceItem ||
|
|
1599
|
+
!(item.id === component.datasourceItem.id &&
|
|
1600
|
+
item.language === component.datasourceItem.language &&
|
|
1601
|
+
item.version === component.datasourceItem.version)),
|
|
1602
|
+
].filter(Boolean);
|
|
1603
|
+
return renderedItems.some((item) => item.id === field.item.id &&
|
|
1604
|
+
item.language === field.item.language &&
|
|
1605
|
+
item.version === field.item.version &&
|
|
1606
|
+
item.renderedFieldIds.includes(field.fieldId));
|
|
1607
|
+
}
|
|
1608
|
+
function isTargetInsideComponent(target, component) {
|
|
1609
|
+
const start = component.firstDOMElement;
|
|
1610
|
+
const end = component.lastDOMElement || component.firstDOMElement;
|
|
1611
|
+
if (!start || !end)
|
|
1612
|
+
return false;
|
|
1613
|
+
if (start.contains(target) || end.contains(target))
|
|
1614
|
+
return true;
|
|
1615
|
+
const startPos = start.compareDocumentPosition(target);
|
|
1616
|
+
const endPos = end.compareDocumentPosition(target);
|
|
1617
|
+
const isAfterStart = !!(startPos & Node.DOCUMENT_POSITION_FOLLOWING) ||
|
|
1618
|
+
!!(startPos & Node.DOCUMENT_POSITION_CONTAINED_BY);
|
|
1619
|
+
const isBeforeEnd = !!(endPos & Node.DOCUMENT_POSITION_PRECEDING) ||
|
|
1620
|
+
!!(endPos & Node.DOCUMENT_POSITION_CONTAINED_BY);
|
|
1621
|
+
return isAfterStart && isBeforeEnd;
|
|
1622
|
+
}
|
|
1623
|
+
function getElementDepth(element) {
|
|
1624
|
+
let depth = 0;
|
|
1625
|
+
let current = element;
|
|
1626
|
+
while (current) {
|
|
1627
|
+
depth++;
|
|
1628
|
+
current = current.parentElement;
|
|
1629
|
+
}
|
|
1630
|
+
return depth;
|
|
1631
|
+
}
|
|
1632
|
+
function injectSXAScripts(iframe) {
|
|
1633
|
+
if (!iframe)
|
|
1634
|
+
return;
|
|
1635
|
+
const iframeDocument = getAccessibleIframeDocument(iframe, "PageViewerFrame.tsx:injectSXAScripts-doc");
|
|
1636
|
+
let iframeWindow;
|
|
1637
|
+
try {
|
|
1638
|
+
iframeWindow = iframe.contentWindow;
|
|
1639
|
+
}
|
|
1640
|
+
catch {
|
|
1641
|
+
iframeWindow = null;
|
|
1642
|
+
}
|
|
1643
|
+
if (!iframeDocument || !iframeWindow)
|
|
1644
|
+
return;
|
|
1645
|
+
iframeWindow.Sitecore = {
|
|
1646
|
+
PageModes: {
|
|
1647
|
+
HoverFrame: {
|
|
1648
|
+
extend: () => {
|
|
1649
|
+
// console.log("extend hoverframe");
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
ChromeManager: {
|
|
1653
|
+
chromes: () => {
|
|
1654
|
+
// console.log("chromes");
|
|
1655
|
+
return [];
|
|
1656
|
+
},
|
|
1657
|
+
resetChromes: () => {
|
|
1658
|
+
// console.log("resetChromes");
|
|
1659
|
+
iframeWindow.XA.init();
|
|
1660
|
+
},
|
|
1661
|
+
chromesReseted: {
|
|
1662
|
+
observe: () => {
|
|
1663
|
+
// console.log("chromesReseted.observe");
|
|
1664
|
+
},
|
|
1665
|
+
},
|
|
1666
|
+
// Dummy selectionChanged with an _callbacks array
|
|
1667
|
+
selectionChanged: {
|
|
1668
|
+
_callbacks: [],
|
|
1669
|
+
// Optionally, you can add a method to trigger all callbacks if needed.
|
|
1670
|
+
trigger: function (...args) {
|
|
1671
|
+
this._callbacks.forEach((callback) => callback(...args));
|
|
1672
|
+
},
|
|
1673
|
+
},
|
|
1674
|
+
},
|
|
1675
|
+
ChromeControls: function () { },
|
|
1676
|
+
ChromeTypes: {
|
|
1677
|
+
PlaceholderSorting: function () { },
|
|
1678
|
+
Rendering: function () { },
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
};
|
|
1682
|
+
// Option 1: Use main window's jQuery if available.
|
|
1683
|
+
if (iframeWindow.jQuery) {
|
|
1684
|
+
// Be cautious: if the iframe's document is different, it's better to load jQuery within the iframe.
|
|
1685
|
+
iframeWindow.$sc = iframeWindow.jQuery;
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
// Option 2: Dynamically load jQuery in the iframe.
|
|
1689
|
+
const jqueryScript = iframeDocument.createElement("script");
|
|
1690
|
+
jqueryScript.type = "text/javascript";
|
|
1691
|
+
jqueryScript.src = "https://code.jquery.com/jquery-3.6.0.min.js"; // Use the version you prefer.
|
|
1692
|
+
jqueryScript.integrity =
|
|
1693
|
+
"sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=";
|
|
1694
|
+
jqueryScript.crossOrigin = "anonymous";
|
|
1695
|
+
jqueryScript.onload = () => {
|
|
1696
|
+
// Once loaded, assign it to $sc using noConflict to avoid global collisions.
|
|
1697
|
+
iframeWindow.$sc = iframeWindow.jQuery.noConflict();
|
|
1698
|
+
// console.log("jQuery loaded in iframe and assigned to $sc");
|
|
1699
|
+
};
|
|
1700
|
+
(iframeDocument.head || iframeDocument.body).appendChild(jqueryScript);
|
|
1701
|
+
}
|
|
1702
|
+
// Add a dummy implementation for renderExpandCommand on the ChromeControls prototype.
|
|
1703
|
+
iframeWindow.Sitecore.PageModes.ChromeControls.prototype.renderExpandCommand =
|
|
1704
|
+
function () {
|
|
1705
|
+
console.log("renderExpandCommand called");
|
|
1706
|
+
// Return a dummy value if needed. For example, you might return a dummy HTML string.
|
|
1707
|
+
return "<div>Dummy Expand Command</div>";
|
|
1708
|
+
};
|
|
1709
|
+
iframeWindow.Sitecore.PageModes.ChromeTypes.PlaceholderSorting.prototype.insertSortingHandle =
|
|
1710
|
+
function () {
|
|
1711
|
+
console.log("insertSortingHandle called");
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Calculates the global offset of a given target node and its local offset,
|
|
1716
|
+
* relative to the container's complete text content.
|
|
1717
|
+
*
|
|
1718
|
+
* @param container - The container element that holds the text nodes.
|
|
1719
|
+
* @param targetNode - The text node where the selection starts or ends.
|
|
1720
|
+
* @param localOffset - The offset within the target text node.
|
|
1721
|
+
* @returns The computed global offset.
|
|
1722
|
+
*/
|
|
1723
|
+
function getGlobalTextOffset(container, targetNode, localOffset) {
|
|
1724
|
+
let globalOffset = 0;
|
|
1725
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
|
|
1726
|
+
while (walker.nextNode()) {
|
|
1727
|
+
const currentNode = walker.currentNode;
|
|
1728
|
+
// If we've reached the target node, add the local offset and return.
|
|
1729
|
+
if (currentNode === targetNode) {
|
|
1730
|
+
return globalOffset + localOffset;
|
|
1731
|
+
}
|
|
1732
|
+
// Otherwise, add the length of this text node.
|
|
1733
|
+
globalOffset += (currentNode.textContent || "").length;
|
|
1734
|
+
}
|
|
1735
|
+
return globalOffset;
|
|
1752
1736
|
}
|
|
1753
1737
|
//# sourceMappingURL=PageViewerFrame.js.map
|