@parhelia/core 0.1.12881 → 0.1.12882
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 +7 -8
- 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 +14 -31
- 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/ContextInfoBar.js +2 -22
- package/dist/editor/ai/terminal/components/ContextInfoBar.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 -144
- 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 +136 -285
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +5 -33
- 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/commands/handlers/uiActionHandlers.js +5 -1
- package/dist/editor/commands/handlers/uiActionHandlers.js.map +1 -1
- package/dist/editor/editor-warnings/FinalWorkflowStateReadOnly.js +5 -0
- package/dist/editor/editor-warnings/FinalWorkflowStateReadOnly.js.map +1 -1
- package/dist/editor/editor-warnings/ItemLocked.js +6 -3
- package/dist/editor/editor-warnings/ItemLocked.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/MediaFolderBrowser.d.ts +2 -1
- package/dist/editor/media-selector/MediaFolderBrowser.js +19 -9
- package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
- package/dist/editor/media-selector/TreeSelector.js +30 -24
- package/dist/editor/media-selector/TreeSelector.js.map +1 -1
- package/dist/editor/media-selector/UploadZone.d.ts +2 -1
- package/dist/editor/media-selector/UploadZone.js +21 -9
- package/dist/editor/media-selector/UploadZone.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 +1509 -1527
- 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/commentAi.js +27 -96
- package/dist/editor/reviews/commentAi.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 -15
- package/dist/editor/services/agentService.js +1 -11
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/contentService.d.ts +1 -0
- package/dist/editor/services/contentService.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/editor/views/MediaFolderEditView.js +1 -1
- package/dist/editor/views/MediaFolderEditView.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
|
@@ -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,38 +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
|
-
const [iframeSrc, setIframeSrc] = useState();
|
|
315
|
-
const [loadedIframeSrc, setLoadedIframeSrc] = useState();
|
|
316
167
|
const [scroll, setScroll] = useState(0);
|
|
317
168
|
const [showMiniMap, setShowMiniMap] = useState(false);
|
|
318
|
-
const [bridgeGeometryRevision, setBridgeGeometryRevision] = useState(0);
|
|
319
|
-
const [bridgeDomRevision, setBridgeDomRevision] = useState(0);
|
|
320
|
-
const bridgeGeometryRevisionRafRef = useRef(null);
|
|
321
|
-
const bridgeDomRevisionRafRef = useRef(null);
|
|
322
169
|
const fieldActionsOverlay = useRef(null);
|
|
323
|
-
const lastSentBridgeCaretRef = useRef(null);
|
|
324
170
|
const [contextMenuFieldButtons, setContextMenuFieldButtons] = useState([]);
|
|
325
171
|
const [contextMenuField, setContextMenuField] = useState();
|
|
326
172
|
const [contextMenuPosition, setContextMenuPosition] = useState();
|
|
327
173
|
const [preSelectedAction, setPreSelectedAction] = useState();
|
|
328
|
-
const bindIframeRef = useCallback((node) => {
|
|
329
|
-
iframeRef.current = node;
|
|
330
|
-
setIframeElement(node);
|
|
331
|
-
}, []);
|
|
332
174
|
// Clear preSelectedAction when overlay is closed
|
|
333
175
|
useEffect(() => {
|
|
334
176
|
if (editContext?.currentOverlay !==
|
|
@@ -337,6 +179,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
337
179
|
}
|
|
338
180
|
}, [editContext?.currentOverlay, contextMenuField?.fieldId]);
|
|
339
181
|
const zoom = pageViewContext.zoom;
|
|
182
|
+
const blockBlurEventRef = useRef(0);
|
|
340
183
|
const [currentItemDescriptor, setCurrentItemDescriptor] = useState(undefined);
|
|
341
184
|
// Field action handlers for the overlay
|
|
342
185
|
const handleActionClick = async (action, event) => {
|
|
@@ -352,1368 +195,1269 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
352
195
|
// Note: handleParameterizedActionFromContextMenu is now created inline in handleContextMenu
|
|
353
196
|
// to avoid React state timing issues with contextMenuPosition
|
|
354
197
|
const pageItemDescriptor = pageViewContext.pageItemDescriptor;
|
|
355
|
-
|
|
356
|
-
const editor = editContextRef.current;
|
|
357
|
-
return !!(showMiniMap &&
|
|
358
|
-
editor?.showMinimap &&
|
|
359
|
-
!editor?.isMobile &&
|
|
360
|
-
editor?.parheliaSettings?.showMinimap !== false);
|
|
361
|
-
}, [showMiniMap]);
|
|
362
|
-
const updateScrollPosition = useCallback((e) => {
|
|
363
|
-
if (!shouldTrackMinimapScroll())
|
|
364
|
-
return;
|
|
365
|
-
setScroll(e);
|
|
366
|
-
if (!compareView)
|
|
367
|
-
pageViewContextRef.current?.setScroll(e);
|
|
368
|
-
}, [compareView, shouldTrackMinimapScroll]);
|
|
369
|
-
const scrollHandler = useThrottledCallback(updateScrollPosition, 100);
|
|
370
|
-
const scrollHandlerRef = useRef(scrollHandler);
|
|
198
|
+
// Update the context whenever the iframe ref changes
|
|
371
199
|
useEffect(() => {
|
|
372
|
-
|
|
373
|
-
}, [
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
200
|
+
pageViewContext.setEditorIframe(iframeRef.current);
|
|
201
|
+
}, [iframeRef.current, pageViewContext.setEditorIframe]);
|
|
202
|
+
const updateMiniMapVisibility = useDebouncedCallback(() => {
|
|
203
|
+
if (!iframeRef.current)
|
|
376
204
|
return;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
});
|
|
381
|
-
}, []);
|
|
382
|
-
const scheduleBridgeDomRevision = useCallback(() => {
|
|
383
|
-
if (bridgeDomRevisionRafRef.current != null)
|
|
205
|
+
const iframe = iframeRef.current;
|
|
206
|
+
const doc = getAccessibleIframeDocument(iframe, "PageViewerFrame.tsx:updateMiniMapVisibility");
|
|
207
|
+
if (!doc?.documentElement)
|
|
384
208
|
return;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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);
|
|
394
222
|
}
|
|
395
|
-
|
|
396
|
-
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (contentHeight > upperThreshold && minimapEnabled) {
|
|
226
|
+
setShowMiniMap(true);
|
|
397
227
|
}
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return (bridgeClientRef.current?.sendCommand("beginInlineEdit", payload) ??
|
|
405
|
-
false);
|
|
406
|
-
}, []);
|
|
407
|
-
const sendApplyRichTextCommand = useCallback((payload) => {
|
|
408
|
-
return (bridgeClientRef.current?.sendCommand("applyRichTextCommand", payload) ??
|
|
409
|
-
false);
|
|
410
|
-
}, []);
|
|
411
|
-
const { beginInlineEdit: beginBridgeInlineEdit, clearInlineDedupe: clearBridgeInlineDedupe, endFieldFocus: endBridgeFieldFocus, handleFieldValueChanged: handleBridgeFieldValueChanged, handleInlineEditEnded: handleBridgeInlineEditEnded, } = useBridgeInlineEditing({
|
|
412
|
-
pageViewContextRef,
|
|
413
|
-
activeBridgeInlineEditRef,
|
|
414
|
-
blockIframeBlurUntil,
|
|
415
|
-
sendBeginInlineEdit,
|
|
228
|
+
}
|
|
229
|
+
}, 100);
|
|
230
|
+
updateMiniMapVisibility();
|
|
231
|
+
const buildPageModelThrottled = useThrottledCallback(buildPageModelSkeleton, 200, {
|
|
232
|
+
leading: true,
|
|
233
|
+
trailing: true,
|
|
416
234
|
});
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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);
|
|
432
272
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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;
|
|
440
286
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}, [endBridgeFieldFocus]);
|
|
447
|
-
useEffect(() => {
|
|
448
|
-
const handleBridgeInlineEditRelease = (event) => {
|
|
449
|
-
const detail = event
|
|
450
|
-
.detail;
|
|
451
|
-
const field = detail?.field;
|
|
452
|
-
const activeEdit = activeBridgeInlineEditRef.current;
|
|
453
|
-
if (!field || !activeEdit)
|
|
454
|
-
return;
|
|
455
|
-
if (!bridgeKeysMatch(activeEdit.fieldId, field.fieldId))
|
|
456
|
-
return;
|
|
457
|
-
if (activeEdit.item && !bridgeItemMatches(activeEdit.item, field.item)) {
|
|
458
|
-
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);
|
|
459
292
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
293
|
+
}, 0);
|
|
294
|
+
};
|
|
295
|
+
// Disconnect the CSS guard on unmount.
|
|
296
|
+
useEffect(() => {
|
|
464
297
|
return () => {
|
|
465
|
-
|
|
298
|
+
try {
|
|
299
|
+
editorCssGuardRef.current.observer?.disconnect();
|
|
300
|
+
}
|
|
301
|
+
catch { }
|
|
302
|
+
editorCssGuardRef.current.observer = null;
|
|
303
|
+
editorCssGuardRef.current.doc = null;
|
|
466
304
|
};
|
|
467
305
|
}, []);
|
|
306
|
+
// If the editor mode flips (edit/preview), re-apply CSS and disable inline editing in the iframe.
|
|
468
307
|
useEffect(() => {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
clearBridgeInlineDedupe();
|
|
477
|
-
},
|
|
478
|
-
});
|
|
479
|
-
}, [
|
|
480
|
-
clearBridgeInlineDedupe,
|
|
481
|
-
editContext,
|
|
482
|
-
endBridgeFieldFocus,
|
|
483
|
-
fieldsContextRef,
|
|
484
|
-
]);
|
|
485
|
-
const getBridgePatchDisplayValue = useCallback(({ field, structureField, itemDescriptor, preferRepositoryValue, }) => {
|
|
486
|
-
const showSuggestions = editContext.mode === "suggestions" || editContext.showSuggestedEdits;
|
|
487
|
-
const repositoryValue = getBridgeFieldPatchValue(field);
|
|
488
|
-
const display = getSuggestionDisplayValue({
|
|
489
|
-
repositoryValue,
|
|
490
|
-
modifiedFields: preferRepositoryValue
|
|
491
|
-
? undefined
|
|
492
|
-
: fieldsContext?.modifiedFields,
|
|
493
|
-
suggestedEdits: editContext.suggestedEdits,
|
|
494
|
-
field: {
|
|
495
|
-
fieldId: field.id || structureField.fieldId,
|
|
496
|
-
itemId: itemDescriptor.id,
|
|
497
|
-
language: itemDescriptor.language,
|
|
498
|
-
version: itemDescriptor.version,
|
|
499
|
-
pageItemId: pageViewContext.pageItemDescriptor?.id,
|
|
500
|
-
pageItemVersion: pageViewContext.pageItemDescriptor?.version,
|
|
501
|
-
},
|
|
502
|
-
mode: showSuggestions ? "suggestions" : "baseline",
|
|
503
|
-
});
|
|
504
|
-
return {
|
|
505
|
-
value: showSuggestions ? display.mergedValue : display.baselineValue,
|
|
506
|
-
source: showSuggestions && display.hasSuggestions
|
|
507
|
-
? "suggestion-preview"
|
|
508
|
-
: "real",
|
|
509
|
-
};
|
|
510
|
-
}, [
|
|
511
|
-
editContext.mode,
|
|
512
|
-
editContext.showSuggestedEdits,
|
|
513
|
-
editContext.suggestedEdits,
|
|
514
|
-
fieldsContext?.modifiedFields,
|
|
515
|
-
pageViewContext.pageItemDescriptor,
|
|
516
|
-
]);
|
|
517
|
-
const buildBridgeFieldPatch = useCallback(({ field, structureField, itemDescriptor, preferRepositoryValue, }) => {
|
|
518
|
-
const display = getBridgePatchDisplayValue({
|
|
519
|
-
field,
|
|
520
|
-
structureField,
|
|
521
|
-
itemDescriptor,
|
|
522
|
-
preferRepositoryValue,
|
|
523
|
-
});
|
|
524
|
-
const activeInlineEdit = activeBridgeInlineEditRef.current;
|
|
525
|
-
const patch = buildBridgeFieldPatchPayload({
|
|
526
|
-
field,
|
|
527
|
-
structureField,
|
|
528
|
-
display,
|
|
529
|
-
activeInlineEdit,
|
|
530
|
-
});
|
|
531
|
-
return patch;
|
|
532
|
-
}, [getBridgePatchDisplayValue]);
|
|
533
|
-
const clearBridgePatchSignatures = useCallback(() => {
|
|
534
|
-
bridgePatchSignatureRef.current.clear();
|
|
535
|
-
}, []);
|
|
536
|
-
const sendBridgeFieldPatch = useCallback((bridge, patch, observedTextContent) => {
|
|
537
|
-
const cacheKey = `${patch.elementKey}:${patch.fieldId}`;
|
|
538
|
-
const signature = JSON.stringify({
|
|
539
|
-
value: patch.value,
|
|
540
|
-
isRichText: !!patch.isRichText,
|
|
541
|
-
source: patch.source ?? "",
|
|
542
|
-
observedTextContent: observedTextContent ?? "",
|
|
543
|
-
});
|
|
544
|
-
if (bridgePatchSignatureRef.current.get(cacheKey) === signature) {
|
|
545
|
-
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);
|
|
546
315
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const editor = editContextRef.current;
|
|
553
|
-
if (!editor || lastSentBridgeCaretRef.current === "clear")
|
|
554
|
-
return;
|
|
555
|
-
editor.sendSocketMessage({
|
|
556
|
-
type: "caret-position",
|
|
557
|
-
payload: { offset: null },
|
|
558
|
-
});
|
|
559
|
-
lastSentBridgeCaretRef.current = "clear";
|
|
560
|
-
}, [editContextRef]);
|
|
561
|
-
const sendBridgeRemoteCaretPosition = useCallback((selection) => {
|
|
562
|
-
const editor = editContextRef.current;
|
|
563
|
-
const activeField = selection.activeField;
|
|
564
|
-
const offset = selection.startOffset ?? selection.endOffset;
|
|
565
|
-
if (!editor ||
|
|
566
|
-
!activeField?.fieldId ||
|
|
567
|
-
!selection.collapsed ||
|
|
568
|
-
offset == null) {
|
|
569
|
-
clearBridgeRemoteCaretPosition();
|
|
316
|
+
}, [editContext.mode]);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!pageItemDescriptor ||
|
|
319
|
+
!pageViewContext.editUrl ||
|
|
320
|
+
!pageViewContext.previewUrl)
|
|
570
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;
|
|
571
333
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
pageViewContextRef.current?.pageItemDescriptor;
|
|
575
|
-
const version = typeof sourceItem?.version === "number" &&
|
|
576
|
-
Number.isFinite(sourceItem.version)
|
|
577
|
-
? sourceItem.version
|
|
578
|
-
: undefined;
|
|
579
|
-
if (!sourceItem?.id || !sourceItem.language || version === undefined) {
|
|
580
|
-
clearBridgeRemoteCaretPosition();
|
|
581
|
-
return;
|
|
334
|
+
catch {
|
|
335
|
+
savedScrollY = 0;
|
|
582
336
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (lastSentBridgeCaretRef.current === nextKey)
|
|
589
|
-
return;
|
|
590
|
-
editor.sendSocketMessage({
|
|
591
|
-
type: "caret-position",
|
|
592
|
-
payload: {
|
|
593
|
-
fieldId: activeField.fieldId,
|
|
594
|
-
item,
|
|
595
|
-
offset,
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
-
lastSentBridgeCaretRef.current = nextKey;
|
|
599
|
-
}, [clearBridgeRemoteCaretPosition, editContextRef, pageViewContextRef]);
|
|
600
|
-
const handleBridgeSelection = useCallback((selection, iframe) => {
|
|
601
|
-
const editor = editContextRef.current;
|
|
602
|
-
if (!editor)
|
|
603
|
-
return;
|
|
604
|
-
const isInlineAiUiFocused = () => {
|
|
605
|
-
const activeEl = document.activeElement;
|
|
606
|
-
return !!(activeEl?.closest(".agent-inline-dialog") ||
|
|
607
|
-
activeEl?.closest(".agent-inline-trigger") ||
|
|
608
|
-
activeEl?.closest('[role="dialog"]'));
|
|
609
|
-
};
|
|
610
|
-
if (selection.collapsed ||
|
|
611
|
-
!selection.text ||
|
|
612
|
-
!selection.activeField?.fieldId) {
|
|
613
|
-
sendBridgeRemoteCaretPosition(selection);
|
|
614
|
-
if (!isInlineAiUiFocused()) {
|
|
615
|
-
editor.setSelectedRange(undefined);
|
|
616
|
-
}
|
|
617
|
-
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);
|
|
618
342
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
pageViewContextRef.current?.pageItemDescriptor;
|
|
623
|
-
if (!item?.id) {
|
|
624
|
-
editor.setSelectedRange(undefined);
|
|
625
|
-
return;
|
|
343
|
+
if (editContext.mode === "preview" && editContext.previewDate) {
|
|
344
|
+
renderUrl.searchParams.delete("sc_version");
|
|
345
|
+
renderUrl.searchParams.set("sc_date", toSitecoreDate(editContext.previewDate));
|
|
626
346
|
}
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
const resolveBridgeFieldDescriptor = useCallback(async (interaction) => {
|
|
662
|
-
const pageView = pageViewContextRef.current;
|
|
663
|
-
const selectedRange = editContextRef.current?.selectedRange;
|
|
664
|
-
const selectedRangeElementKey = selectedRange?.elementKey;
|
|
665
|
-
const selectedRangeMatchesInteraction = !!((interaction.kind === "contextMenu" ||
|
|
666
|
-
interaction.kind === "keydown") &&
|
|
667
|
-
selectedRange?.text &&
|
|
668
|
-
selectedRange.fieldId &&
|
|
669
|
-
((selectedRangeElementKey &&
|
|
670
|
-
interaction.elementKey &&
|
|
671
|
-
selectedRangeElementKey === interaction.elementKey) ||
|
|
672
|
-
bridgeIdsMatch(selectedRange.fieldId, interaction.fieldId) ||
|
|
673
|
-
bridgeIdsMatch(selectedRange.itemId, interaction.item?.id)));
|
|
674
|
-
const fieldId = selectedRangeMatchesInteraction
|
|
675
|
-
? selectedRange?.fieldId
|
|
676
|
-
: interaction.fieldId;
|
|
677
|
-
if (!fieldId)
|
|
678
|
-
return undefined;
|
|
679
|
-
const targetElementKey = selectedRangeMatchesInteraction
|
|
680
|
-
? selectedRangeElementKey
|
|
681
|
-
: interaction.elementKey;
|
|
682
|
-
const structureFields = pageView?.bridgeStructure?.fields ?? [];
|
|
683
|
-
const structureField = structureFields.find((field) => bridgeIdsMatch(field.fieldId, fieldId)) ??
|
|
684
|
-
structureFields.find((field) => targetElementKey && field.elementKey === targetElementKey);
|
|
685
|
-
const fallbackItem = pageView?.page?.item ?? pageView?.pageItemDescriptor;
|
|
686
|
-
const componentId = resolveBridgeComponentId(interaction.componentId ?? structureField?.componentId, pageView?.page);
|
|
687
|
-
const component = componentId && pageView?.page
|
|
688
|
-
? getComponentById(componentId, pageView.page)
|
|
689
|
-
: undefined;
|
|
690
|
-
const candidateItems = [];
|
|
691
|
-
const selectedRangeItem = selectedRangeMatchesInteraction &&
|
|
692
|
-
selectedRange?.itemId &&
|
|
693
|
-
selectedRange.language &&
|
|
694
|
-
typeof selectedRange.version === "number"
|
|
695
|
-
? {
|
|
696
|
-
id: selectedRange.itemId,
|
|
697
|
-
language: selectedRange.language,
|
|
698
|
-
version: selectedRange.version,
|
|
699
|
-
database: fallbackItem?.database,
|
|
700
|
-
}
|
|
701
|
-
: undefined;
|
|
702
|
-
const addCandidateItem = (source) => {
|
|
703
|
-
const itemId = source?.id ?? fallbackItem?.id;
|
|
704
|
-
const language = source?.language ?? fallbackItem?.language;
|
|
705
|
-
const versionValue = source?.version ?? fallbackItem?.version;
|
|
706
|
-
const version = typeof versionValue === "number" && Number.isFinite(versionValue)
|
|
707
|
-
? versionValue
|
|
708
|
-
: undefined;
|
|
709
|
-
if (!itemId || !language || version === undefined)
|
|
710
|
-
return;
|
|
711
|
-
if (candidateItems.some((item) => bridgeIdsMatch(item.id, itemId) &&
|
|
712
|
-
item.language.toLowerCase() === language.toLowerCase() &&
|
|
713
|
-
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();
|
|
714
381
|
return;
|
|
715
382
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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);
|
|
739
409
|
};
|
|
410
|
+
restoreScroll(50);
|
|
411
|
+
restoreScroll(200);
|
|
412
|
+
restoreScroll(500);
|
|
413
|
+
restoreScroll(1000);
|
|
414
|
+
restoreScroll(2000);
|
|
740
415
|
}
|
|
741
|
-
}
|
|
742
|
-
const fallbackDescriptorItem = candidateItems[0];
|
|
743
|
-
if (!fallbackDescriptorItem)
|
|
744
|
-
return undefined;
|
|
745
|
-
return {
|
|
746
|
-
fieldId,
|
|
747
|
-
item: fallbackDescriptorItem,
|
|
748
416
|
};
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
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();
|
|
768
435
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
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);
|
|
773
441
|
}
|
|
774
|
-
return;
|
|
775
442
|
}
|
|
776
|
-
|
|
777
|
-
|
|
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;
|
|
778
448
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (interaction.kind === "contextMenu" && interaction.ctrlKey)
|
|
782
|
-
return;
|
|
783
|
-
const editor = editContextRef.current;
|
|
784
|
-
if (!editor)
|
|
785
|
-
return;
|
|
786
|
-
const selectFromBridgeInteraction = (ids) => {
|
|
787
|
-
if (!bridgeComponentSelectionsMatch(editor.selection, ids)) {
|
|
788
|
-
suppressNextSelectionScrollRef.current = true;
|
|
789
|
-
}
|
|
790
|
-
editor.select(ids);
|
|
791
|
-
};
|
|
792
|
-
let effectiveInteraction = interaction;
|
|
793
|
-
if (interaction.kind === "contextMenu" &&
|
|
794
|
-
!interaction.fieldId &&
|
|
795
|
-
interaction.clientX !== undefined &&
|
|
796
|
-
interaction.clientY !== undefined) {
|
|
797
|
-
const fieldTarget = findBridgeFieldTargetAtPoint(pageViewContextRef.current?.bridgeGeometry, interaction.clientX, interaction.clientY);
|
|
798
|
-
if (fieldTarget?.fieldId) {
|
|
799
|
-
effectiveInteraction = {
|
|
800
|
-
...interaction,
|
|
801
|
-
elementKey: fieldTarget.elementKey ?? interaction.elementKey,
|
|
802
|
-
fieldId: fieldTarget.fieldId,
|
|
803
|
-
};
|
|
804
|
-
}
|
|
449
|
+
catch {
|
|
450
|
+
integrationRefreshFn = undefined;
|
|
805
451
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
effectiveInteraction.fieldId &&
|
|
809
|
-
effectiveInteraction.elementKey &&
|
|
810
|
-
!effectiveInteraction.ctrlKey &&
|
|
811
|
-
!effectiveInteraction.shiftKey &&
|
|
812
|
-
!effectiveInteraction.metaKey);
|
|
813
|
-
const currentOverlay = editor.currentOverlay;
|
|
814
|
-
const isGeneratorOverlay = !!(currentOverlay &&
|
|
815
|
-
typeof currentOverlay === "string" &&
|
|
816
|
-
currentOverlay.endsWith("_generators"));
|
|
817
|
-
if (interaction.kind === "click" && currentOverlay === "context-menu") {
|
|
818
|
-
editor.setCurrentOverlay(undefined);
|
|
452
|
+
if (!versionChanged && integrationRefreshFn) {
|
|
453
|
+
runIntegrationRefresh(integrationRefreshFn, "Integration - requesting refresh");
|
|
819
454
|
}
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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);
|
|
824
474
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
selectFromBridgeInteraction([]);
|
|
828
|
-
if (isEditableFieldClick) {
|
|
829
|
-
await beginTrackedBridgeInlineEdit(effectiveInteraction);
|
|
830
|
-
}
|
|
831
|
-
else if (interaction.kind === "click") {
|
|
832
|
-
endTrackedBridgeFieldFocus();
|
|
833
|
-
}
|
|
834
|
-
return;
|
|
475
|
+
else {
|
|
476
|
+
runFallbackRefresh();
|
|
835
477
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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])
|
|
495
|
+
return;
|
|
496
|
+
let fieldElement;
|
|
497
|
+
try {
|
|
498
|
+
fieldElement = findFieldElement(iframeRef.current, fieldsContext.focusedField);
|
|
840
499
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
: [componentId];
|
|
844
|
-
if (!currentSelection.includes(componentId)) {
|
|
845
|
-
selectFromBridgeInteraction(selectedIds);
|
|
500
|
+
catch {
|
|
501
|
+
fieldElement = null;
|
|
846
502
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
clientY,
|
|
869
|
-
button: 2,
|
|
870
|
-
});
|
|
871
|
-
setContextMenuPosition(menuPosition);
|
|
872
|
-
let loadingShown = false;
|
|
873
|
-
const loadingTimer = window.setTimeout(() => {
|
|
874
|
-
loadingShown = true;
|
|
875
|
-
editor.showContextMenu(adjustedEvent, [
|
|
876
|
-
{
|
|
877
|
-
id: "loading",
|
|
878
|
-
label: "Loading...",
|
|
879
|
-
disabled: true,
|
|
880
|
-
icon: _jsx(Spinner, { size: "sm", className: "mr-2" }),
|
|
881
|
-
},
|
|
882
|
-
]);
|
|
883
|
-
}, 100);
|
|
884
|
-
const field = await resolveBridgeFieldDescriptor(effectiveInteraction);
|
|
885
|
-
const fieldButtons = field ? await loadFieldButtons(field) : [];
|
|
886
|
-
const handleParameterizedActionWithPosition = (field, action) => {
|
|
887
|
-
setContextMenuField(field);
|
|
888
|
-
setContextMenuFieldButtons([action]);
|
|
889
|
-
setPreSelectedAction(action);
|
|
890
|
-
window.setTimeout(() => {
|
|
891
|
-
const tempElement = document.createElement("div");
|
|
892
|
-
tempElement.style.position = "fixed";
|
|
893
|
-
tempElement.style.left = menuPosition.x + "px";
|
|
894
|
-
tempElement.style.top = menuPosition.y + "px";
|
|
895
|
-
tempElement.style.width = "1px";
|
|
896
|
-
tempElement.style.height = "1px";
|
|
897
|
-
tempElement.style.visibility = "hidden";
|
|
898
|
-
tempElement.style.pointerEvents = "none";
|
|
899
|
-
document.body.appendChild(tempElement);
|
|
900
|
-
const positionedEvent = new MouseEvent("click", {
|
|
901
|
-
bubbles: true,
|
|
902
|
-
cancelable: true,
|
|
903
|
-
view: window,
|
|
904
|
-
clientX: menuPosition.x,
|
|
905
|
-
clientY: menuPosition.y,
|
|
906
|
-
});
|
|
907
|
-
Object.defineProperty(positionedEvent, "target", {
|
|
908
|
-
value: tempElement,
|
|
909
|
-
enumerable: true,
|
|
910
|
-
});
|
|
911
|
-
fieldActionsOverlay.current?.show(positionedEvent, action);
|
|
912
|
-
window.setTimeout(() => {
|
|
913
|
-
tempElement.remove();
|
|
914
|
-
}, 1000);
|
|
915
|
-
}, 100);
|
|
916
|
-
};
|
|
917
|
-
const items = await buildComponentContextMenuItems(selectedComponents, editor, field, fieldButtons, handleParameterizedActionWithPosition);
|
|
918
|
-
window.clearTimeout(loadingTimer);
|
|
919
|
-
if (loadingShown) {
|
|
920
|
-
editor.updateContextMenu(items);
|
|
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
|
+
}
|
|
921
524
|
}
|
|
922
|
-
|
|
923
|
-
|
|
525
|
+
}
|
|
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);
|
|
924
532
|
}
|
|
925
|
-
return;
|
|
926
533
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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;
|
|
942
576
|
}
|
|
943
|
-
|
|
577
|
+
doc.open();
|
|
578
|
+
doc.write(text);
|
|
579
|
+
doc.close();
|
|
944
580
|
}
|
|
945
581
|
else {
|
|
946
|
-
|
|
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);
|
|
947
609
|
}
|
|
610
|
+
ensureEditorCssGuard(doc, editContext.mode !== "preview");
|
|
611
|
+
setTimeout(() => {
|
|
612
|
+
injectSXAScripts(iframeRef.current);
|
|
613
|
+
}, 1000);
|
|
614
|
+
try {
|
|
615
|
+
requestPageModelBuild(doc);
|
|
616
|
+
}
|
|
617
|
+
catch (buildErr) { }
|
|
948
618
|
}
|
|
949
|
-
|
|
950
|
-
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
if (err?.name === "AbortError") {
|
|
622
|
+
// Swallow aborts – a newer refresh superseded this one
|
|
623
|
+
return;
|
|
951
624
|
}
|
|
625
|
+
throw err;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
const iframeDoc = getAccessibleIframeDocument(iframeRef.current, "PageViewerFrame.tsx:scrollIntoView-doc");
|
|
630
|
+
if (!editContext.scrollIntoView || !iframeDoc?.documentElement)
|
|
631
|
+
return;
|
|
632
|
+
const component = getComponentById(editContext.scrollIntoView, pageViewContextRef.current.page);
|
|
633
|
+
if (!component)
|
|
952
634
|
return;
|
|
635
|
+
let rect;
|
|
636
|
+
try {
|
|
637
|
+
rect = findComponentRect(iframeRef.current, component, true);
|
|
953
638
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
else {
|
|
959
|
-
selectFromBridgeInteraction([...currentSelection, componentId]);
|
|
960
|
-
}
|
|
639
|
+
catch {
|
|
640
|
+
rect = undefined;
|
|
641
|
+
}
|
|
642
|
+
if (!rect)
|
|
961
643
|
return;
|
|
644
|
+
if (!iframeRef.current)
|
|
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;
|
|
962
651
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
await beginTrackedBridgeInlineEdit(effectiveInteraction);
|
|
652
|
+
catch {
|
|
653
|
+
scrollTop = 0;
|
|
966
654
|
}
|
|
967
|
-
|
|
968
|
-
|
|
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;
|
|
969
661
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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]);
|
|
979
672
|
useEffect(() => {
|
|
980
|
-
const
|
|
981
|
-
|
|
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
|
+
}
|
|
705
|
+
return;
|
|
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())) {
|
|
982
710
|
return;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
711
|
+
}
|
|
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
|
+
}
|
|
987
718
|
return;
|
|
988
719
|
}
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
getUrlOrigin(pageViewContext.previewUrl),
|
|
992
|
-
].filter((origin) => Boolean(origin));
|
|
993
|
-
if (allowedHostOrigins.length === 0) {
|
|
994
|
-
pageViewContext.setBridgeReady(false);
|
|
995
|
-
pageViewContext.setIframeSupportsRefresh(false);
|
|
720
|
+
const fieldId = fieldElement.getAttribute("data-fieldid");
|
|
721
|
+
if (!fieldId)
|
|
996
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;
|
|
997
731
|
}
|
|
998
|
-
//
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const
|
|
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
|
-
|
|
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 },
|
|
1044
788
|
},
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
bridge.sendCommand("setZoom", {
|
|
1049
|
-
zoom: pageViewContextRef.current?.zoom ?? 1,
|
|
789
|
+
originatingSlotId: slotContext?.slotId,
|
|
790
|
+
refresh: "none",
|
|
791
|
+
value: valueToSave,
|
|
1050
792
|
});
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
case "pageSkeletonUpdated":
|
|
1056
|
-
pageViewContext.setPageSkeleton(event.payload.pageSkeleton);
|
|
1057
|
-
break;
|
|
1058
|
-
case "geometryUpdated":
|
|
1059
|
-
latestBridgeScrollRef.current = event.payload.geometry.scroll;
|
|
1060
|
-
pageViewContext.bridgeGeometry = event.payload.geometry;
|
|
1061
|
-
pageViewContext.setBridgeGeometry(event.payload.geometry);
|
|
1062
|
-
scheduleBridgeGeometryRevision();
|
|
1063
|
-
dispatchBridgeOverlayScroll(iframe, event.payload.geometry.scroll, getBridgeGeometryScrollScale(event.payload.geometry));
|
|
1064
|
-
dispatchBridgeOverlayGeometry(iframe, event.payload.geometry);
|
|
1065
|
-
if (shouldTrackMinimapScroll()) {
|
|
1066
|
-
scrollHandlerRef.current(event.payload.geometry.scroll.y);
|
|
1067
|
-
}
|
|
1068
|
-
break;
|
|
1069
|
-
case "domUpdated":
|
|
1070
|
-
pageViewContext.bridgeDom = event.payload;
|
|
1071
|
-
pageViewContext.setBridgeDom(event.payload);
|
|
1072
|
-
scheduleBridgeDomRevision();
|
|
1073
|
-
window.dispatchEvent(new CustomEvent(BRIDGE_DOM_UPDATED_EVENT));
|
|
1074
|
-
break;
|
|
1075
|
-
case "refreshStarted":
|
|
1076
|
-
clearBridgePatchSignatures();
|
|
1077
|
-
activeBridgeInlineEditRef.current = null;
|
|
1078
|
-
break;
|
|
1079
|
-
case "refreshCompleted":
|
|
1080
|
-
// Runtime refresh replaces host DOM without a new bridge ready event.
|
|
1081
|
-
// Field patches sent during the refresh window may have been cached
|
|
1082
|
-
// against the previous DOM generation, so force the refreshed host to
|
|
1083
|
-
// receive the current repository values again.
|
|
1084
|
-
clearBridgePatchSignatures();
|
|
1085
|
-
if (event.payload.structure) {
|
|
1086
|
-
pageViewContext.setBridgeStructure(event.payload.structure);
|
|
1087
|
-
}
|
|
1088
|
-
if (event.payload.geometry) {
|
|
1089
|
-
latestBridgeScrollRef.current = event.payload.geometry.scroll;
|
|
1090
|
-
pageViewContext.bridgeGeometry = event.payload.geometry;
|
|
1091
|
-
pageViewContext.setBridgeGeometry(event.payload.geometry);
|
|
1092
|
-
scheduleBridgeGeometryRevision();
|
|
1093
|
-
dispatchBridgeOverlayScroll(iframe, event.payload.geometry.scroll, getBridgeGeometryScrollScale(event.payload.geometry));
|
|
1094
|
-
dispatchBridgeOverlayGeometry(iframe, event.payload.geometry);
|
|
1095
|
-
if (shouldTrackMinimapScroll()) {
|
|
1096
|
-
scrollHandlerRef.current(event.payload.geometry.scroll.y);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
setShowSpinner(false);
|
|
1100
|
-
break;
|
|
1101
|
-
case "selectionChanged":
|
|
1102
|
-
pageViewContext.setBridgeSelection(event.payload.selection);
|
|
1103
|
-
handleBridgeSelection(event.payload.selection, iframe);
|
|
1104
|
-
break;
|
|
1105
|
-
case "fieldValueChanged":
|
|
1106
|
-
handleBridgeFieldValueChanged(event.payload);
|
|
1107
|
-
break;
|
|
1108
|
-
case "inlineEditEnded":
|
|
1109
|
-
handleBridgeInlineEditEnded(event.payload);
|
|
1110
|
-
activeBridgeInlineEditRef.current = null;
|
|
1111
|
-
break;
|
|
1112
|
-
case "interaction":
|
|
1113
|
-
pageViewContext.setBridgeInteraction(event.payload);
|
|
1114
|
-
void handleBridgeInteraction(event.payload, iframe);
|
|
1115
|
-
break;
|
|
1116
|
-
case "scrollChanged":
|
|
1117
|
-
latestBridgeScrollRef.current = event.payload.scroll;
|
|
1118
|
-
dispatchBridgeOverlayScroll(iframe, event.payload.scroll, getBridgeGeometryScrollScale(pageViewContextRef.current?.bridgeGeometry));
|
|
1119
|
-
if (shouldTrackMinimapScroll()) {
|
|
1120
|
-
scrollHandlerRef.current(event.payload.scroll.y);
|
|
1121
|
-
}
|
|
1122
|
-
break;
|
|
1123
|
-
case "renderError":
|
|
1124
|
-
console.warn("[Parhelia bridge] Host render error", event.payload);
|
|
1125
|
-
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
console.error("[PageViewerFrame] Failed to replace text:", error);
|
|
1126
797
|
}
|
|
1127
798
|
},
|
|
1128
799
|
});
|
|
1129
|
-
|
|
1130
|
-
pageViewContext.setBridgeReady(false);
|
|
1131
|
-
client.connect();
|
|
1132
|
-
return () => {
|
|
1133
|
-
if (bridgeClientRef.current === client) {
|
|
1134
|
-
bridgeClientRef.current = null;
|
|
1135
|
-
}
|
|
1136
|
-
client.disconnect();
|
|
1137
|
-
clearBridgePatchSignatures();
|
|
1138
|
-
activeBridgeInlineEditRef.current = null;
|
|
1139
|
-
pageViewContext.setBridgeReady(false);
|
|
1140
|
-
pageViewContext.setIframeSupportsRefresh(false);
|
|
1141
|
-
};
|
|
1142
|
-
}, [
|
|
1143
|
-
iframeElement,
|
|
1144
|
-
iframeSrc,
|
|
1145
|
-
loadedIframeSrc,
|
|
1146
|
-
compareView,
|
|
1147
|
-
handleBridgeFieldValueChanged,
|
|
1148
|
-
handleBridgeInlineEditEnded,
|
|
1149
|
-
handleBridgeInteraction,
|
|
1150
|
-
handleBridgeSelection,
|
|
1151
|
-
clearBridgePatchSignatures,
|
|
1152
|
-
pageViewContext.editUrl,
|
|
1153
|
-
pageViewContext.previewUrl,
|
|
1154
|
-
scheduleBridgeDomRevision,
|
|
1155
|
-
scheduleBridgeGeometryRevision,
|
|
1156
|
-
shouldTrackMinimapScroll,
|
|
1157
|
-
]);
|
|
800
|
+
}, 300);
|
|
1158
801
|
useEffect(() => {
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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)
|
|
1165
813
|
return;
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
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)
|
|
1169
824
|
return;
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
+
}
|
|
1182
890
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
891
|
+
else {
|
|
892
|
+
editContextRef.current?.select([componentId]);
|
|
1185
893
|
}
|
|
1186
|
-
const patch = buildBridgeFieldPatch({
|
|
1187
|
-
field,
|
|
1188
|
-
structureField,
|
|
1189
|
-
itemDescriptor: change.item,
|
|
1190
|
-
preferRepositoryValue: change.source === "local-field-update",
|
|
1191
|
-
});
|
|
1192
|
-
if (!patch)
|
|
1193
|
-
continue;
|
|
1194
|
-
patchedElementKeys.add(structureField.elementKey);
|
|
1195
|
-
patches.push(patch);
|
|
1196
|
-
}
|
|
1197
|
-
for (const patch of patches) {
|
|
1198
|
-
if (disposed)
|
|
1199
|
-
return;
|
|
1200
|
-
const structureField = structure.fields.find((field) => field.elementKey === patch.elementKey &&
|
|
1201
|
-
field.fieldId === patch.fieldId);
|
|
1202
|
-
sendBridgeFieldPatch(bridge, patch, structureField?.textContent);
|
|
1203
894
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
return;
|
|
1215
|
-
const localField = localFieldsById.get(changedFieldId.toLowerCase());
|
|
1216
|
-
if (!localField) {
|
|
1217
|
-
needsAsyncLookup = true;
|
|
1218
|
-
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);
|
|
1219
905
|
}
|
|
1220
|
-
processResolvedField(change, changedFieldId, localField);
|
|
1221
906
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
907
|
+
else {
|
|
908
|
+
// Regular click: always select just this component
|
|
909
|
+
editContextRef.current?.select([componentId]);
|
|
1224
910
|
}
|
|
911
|
+
// if (mode !== "edit") {
|
|
912
|
+
//editContextRef.current?.setScrollIntoView(componentId);
|
|
913
|
+
//}
|
|
1225
914
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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) {
|
|
1235
931
|
return;
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
|
1245
941
|
}
|
|
1246
942
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
unsubscribe();
|
|
1256
|
-
};
|
|
1257
|
-
}, [
|
|
1258
|
-
buildBridgeFieldPatch,
|
|
1259
|
-
editContext.itemsRepository,
|
|
1260
|
-
sendBridgeFieldPatch,
|
|
1261
|
-
]);
|
|
1262
|
-
useEffect(() => {
|
|
1263
|
-
const bridge = bridgeClientRef.current;
|
|
1264
|
-
const structure = pageViewContext.bridgeStructure;
|
|
1265
|
-
if (!pageViewContext.bridgeReady || !bridge || !structure?.fields.length) {
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
let disposed = false;
|
|
1269
|
-
void (async () => {
|
|
1270
|
-
const patchedElementKeys = new Set();
|
|
1271
|
-
for (const structureField of structure.fields) {
|
|
1272
|
-
if (disposed)
|
|
1273
|
-
return;
|
|
1274
|
-
if (!structureField.elementKey)
|
|
1275
|
-
continue;
|
|
1276
|
-
if (patchedElementKeys.has(structureField.elementKey))
|
|
1277
|
-
continue;
|
|
1278
|
-
const fallbackItem = pageViewContextRef.current?.pageItemDescriptor ??
|
|
1279
|
-
pageViewContextRef.current?.page?.item?.descriptor;
|
|
1280
|
-
const bridgeItem = structureField.item;
|
|
1281
|
-
const itemDescriptor = {
|
|
1282
|
-
id: bridgeItem?.id ?? fallbackItem?.id,
|
|
1283
|
-
language: bridgeItem?.language ?? fallbackItem?.language,
|
|
1284
|
-
version: typeof bridgeItem?.version === "number"
|
|
1285
|
-
? bridgeItem.version
|
|
1286
|
-
: fallbackItem?.version,
|
|
1287
|
-
};
|
|
1288
|
-
if (!itemDescriptor.id ||
|
|
1289
|
-
!itemDescriptor.language ||
|
|
1290
|
-
typeof itemDescriptor.version !== "number") {
|
|
1291
|
-
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
|
+
}
|
|
1292
951
|
}
|
|
1293
|
-
const loadedItem = await editContext.itemsRepository.getItem(itemDescriptor);
|
|
1294
|
-
if (disposed || !loadedItem)
|
|
1295
|
-
continue;
|
|
1296
|
-
const field = loadedItem.fields.find((candidate) => fieldIdentifierMatches(candidate, structureField.fieldId));
|
|
1297
|
-
if (!field)
|
|
1298
|
-
continue;
|
|
1299
|
-
const patch = buildBridgeFieldPatch({
|
|
1300
|
-
field,
|
|
1301
|
-
structureField,
|
|
1302
|
-
itemDescriptor: itemDescriptor,
|
|
1303
|
-
});
|
|
1304
|
-
if (!patch)
|
|
1305
|
-
continue;
|
|
1306
|
-
patchedElementKeys.add(structureField.elementKey);
|
|
1307
|
-
sendBridgeFieldPatch(bridge, patch, structureField.textContent);
|
|
1308
952
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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);
|
|
1312
962
|
}
|
|
1313
|
-
});
|
|
1314
|
-
return () => {
|
|
1315
|
-
disposed = true;
|
|
1316
963
|
};
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
editContext.itemsRepository.revision,
|
|
1321
|
-
editContext.mode,
|
|
1322
|
-
editContext.showSuggestedEdits,
|
|
1323
|
-
editContext.suggestedEdits,
|
|
1324
|
-
fieldsContext?.modifiedFields,
|
|
1325
|
-
bridgeDomRevision,
|
|
1326
|
-
pageViewContext.bridgeReady,
|
|
1327
|
-
pageViewContext.bridgeStructure,
|
|
1328
|
-
sendBridgeFieldPatch,
|
|
1329
|
-
]);
|
|
1330
|
-
useEffect(() => {
|
|
1331
|
-
const requestBridgeGeometry = (payload) => {
|
|
1332
|
-
const bridge = bridgeClientRef.current;
|
|
1333
|
-
if (!bridge)
|
|
1334
|
-
return false;
|
|
1335
|
-
// The editing host only remembers the most recent queryGeometry payload.
|
|
1336
|
-
// Comments, suggestions, locks and version-diff each contribute text
|
|
1337
|
-
// ranges independently, so without merging they clobbered one another and
|
|
1338
|
-
// fought in an endless re-request loop (heavy highlight flicker). Keep the
|
|
1339
|
-
// latest ranges per `source` and always query with their union.
|
|
1340
|
-
if (payload && typeof payload.source === "string") {
|
|
1341
|
-
const { source, textRanges, ...rest } = payload;
|
|
1342
|
-
const sources = bridgeTextRangeSourcesRef.current;
|
|
1343
|
-
if (textRanges && textRanges.length > 0) {
|
|
1344
|
-
sources.set(source, textRanges);
|
|
1345
|
-
}
|
|
1346
|
-
else {
|
|
1347
|
-
sources.delete(source);
|
|
1348
|
-
}
|
|
1349
|
-
const mergedTextRanges = Array.from(sources.values()).flat();
|
|
1350
|
-
return (bridge.sendCommand("queryGeometry", {
|
|
1351
|
-
...rest,
|
|
1352
|
-
textRanges: mergedTextRanges,
|
|
1353
|
-
}) ?? false);
|
|
1354
|
-
}
|
|
1355
|
-
return bridge.sendCommand("queryGeometry", payload) ?? false;
|
|
964
|
+
const handleIframeScroll = () => {
|
|
965
|
+
const scrollTop = boundScrollContainer?.scrollTop || 0;
|
|
966
|
+
scrollHandler(scrollTop);
|
|
1356
967
|
};
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
968
|
+
const handleIframeWindowScroll = () => {
|
|
969
|
+
const scrollTop = boundScrollContainer?.scrollTop || 0;
|
|
970
|
+
scrollHandler(scrollTop);
|
|
1360
971
|
};
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
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));
|
|
1364
983
|
};
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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;
|
|
1000
|
+
}
|
|
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
|
+
}
|
|
1370
1030
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1031
|
+
};
|
|
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);
|
|
1373
1056
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
pageViewContext.requestBridgeRichTextCommand = undefined;
|
|
1057
|
+
else {
|
|
1058
|
+
blockBlurEventRef.current = 0;
|
|
1377
1059
|
}
|
|
1378
1060
|
};
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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;
|
|
1385
1069
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1070
|
+
editContextRef.current?.operations.onFieldBlur?.();
|
|
1071
|
+
fieldsContextRef.current?.setInlineEditingFieldElement(undefined);
|
|
1072
|
+
fieldsContextRef.current?.setFocusedField(undefined, false);
|
|
1389
1073
|
};
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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);
|
|
1394
1081
|
}
|
|
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;
|
|
1395
1098
|
};
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const lowerThreshold = clientHeight; // hide minimap if content falls below this
|
|
1410
|
-
// Check if minimap is enabled in settings and user controls
|
|
1411
|
-
const minimapEnabled = editContext.parheliaSettings?.showMinimap !== false &&
|
|
1412
|
-
editContext.showMinimap;
|
|
1413
|
-
if (showMiniMap) {
|
|
1414
|
-
if (contentHeight <= lowerThreshold || !minimapEnabled) {
|
|
1415
|
-
setShowMiniMap(false);
|
|
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) {
|
|
1111
|
+
return;
|
|
1416
1112
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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);
|
|
1421
1120
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
bridgeGeometryRevision,
|
|
1429
|
-
editContext.parheliaSettings?.showMinimap,
|
|
1430
|
-
editContext.showMinimap,
|
|
1431
|
-
iframeElement,
|
|
1432
|
-
showMiniMap,
|
|
1433
|
-
updateMiniMapVisibility,
|
|
1434
|
-
]);
|
|
1435
|
-
// If the editor mode flips into preview, clear any active text selection state.
|
|
1436
|
-
useEffect(() => {
|
|
1437
|
-
const isPreview = editContext.mode === "preview";
|
|
1438
|
-
if (isPreview) {
|
|
1439
|
-
editContextRef.current?.setSelectedRange(undefined);
|
|
1440
|
-
}
|
|
1441
|
-
}, [editContext.mode]);
|
|
1442
|
-
useEffect(() => {
|
|
1443
|
-
if (!pageItemDescriptor ||
|
|
1444
|
-
!pageViewContext.editUrl ||
|
|
1445
|
-
!pageViewContext.previewUrl)
|
|
1446
|
-
return;
|
|
1447
|
-
const urlPath = editContext.mode === "preview"
|
|
1448
|
-
? pageViewContext.previewUrl
|
|
1449
|
-
: pageViewContext.editUrl;
|
|
1450
|
-
const renderUrl = new URL(urlPath, window.location.origin);
|
|
1451
|
-
alignLoopbackHostToEditor(renderUrl);
|
|
1452
|
-
const editRev = uuid();
|
|
1453
|
-
renderUrl.searchParams.set("edit_rev", editRev);
|
|
1454
|
-
if (editContext.mode !== "preview" && editContext.sessionId) {
|
|
1455
|
-
renderUrl.searchParams.set("parhelia_session", editContext.sessionId);
|
|
1456
|
-
}
|
|
1457
|
-
if (editContext.mode === "preview" && editContext.previewDate) {
|
|
1458
|
-
renderUrl.searchParams.delete("sc_version");
|
|
1459
|
-
renderUrl.searchParams.set("sc_date", toSitecoreDate(editContext.previewDate));
|
|
1460
|
-
}
|
|
1461
|
-
// Layout-mode marker. Only set when editing shared layout, paired with the
|
|
1462
|
-
// `parhelia` editor marker so ParheliaSetLayoutRenderings ignores any unrelated
|
|
1463
|
-
// requests that happen to carry parhelia_layout.
|
|
1464
|
-
if (editContext.mode !== "preview" && editContext.layoutMode === "shared") {
|
|
1465
|
-
renderUrl.searchParams.set("parhelia", "1");
|
|
1466
|
-
renderUrl.searchParams.set("parhelia_layout", "shared");
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
renderUrl.searchParams.delete("parhelia_layout");
|
|
1470
|
-
}
|
|
1471
|
-
// Detect if the version in the URL changed - this requires a full reload, not just requestRefresh
|
|
1472
|
-
// because Next.js router.replace may not properly refetch server data for version changes.
|
|
1473
|
-
const currentIframeUrl = iframeRef.current?.src;
|
|
1474
|
-
const currentIframeOrigin = getUrlOrigin(currentIframeUrl);
|
|
1475
|
-
const renderUrlIsCrossOrigin = renderUrl.origin !== window.location.origin;
|
|
1476
|
-
const iframeOriginChanged = !!(currentIframeOrigin && currentIframeOrigin !== renderUrl.origin);
|
|
1477
|
-
const currentVersion = currentIframeUrl
|
|
1478
|
-
? new URL(currentIframeUrl).searchParams.get("sc_version")
|
|
1479
|
-
: null;
|
|
1480
|
-
const newVersion = renderUrl.searchParams.get("sc_version");
|
|
1481
|
-
const versionChanged = (currentVersion !== null || newVersion !== null) &&
|
|
1482
|
-
currentVersion !== newVersion;
|
|
1483
|
-
const initialLoad = currentItemDescriptor?.id !== pageItemDescriptor.id ||
|
|
1484
|
-
currentItemDescriptor?.language !== pageItemDescriptor.language ||
|
|
1485
|
-
currentItemDescriptor?.version !== pageItemDescriptor.version;
|
|
1486
|
-
const shouldUseIframeSrcReload = initialLoad || pageViewContext.fullscreen || iframeOriginChanged;
|
|
1487
|
-
function runFallbackRefresh() {
|
|
1488
|
-
pageViewContext.setIframeSupportsRefresh(false);
|
|
1489
|
-
setShowSpinner(true);
|
|
1490
|
-
console.log(initialLoad
|
|
1491
|
-
? "Initial load - setting iframe src"
|
|
1492
|
-
: renderUrlIsCrossOrigin
|
|
1493
|
-
? "Cross-origin load - setting iframe src"
|
|
1494
|
-
: iframeOriginChanged
|
|
1495
|
-
? "Iframe origin changed - setting iframe src"
|
|
1496
|
-
: "Reloading iframe src");
|
|
1497
|
-
setLoadedIframeSrc(undefined);
|
|
1498
|
-
setIframeSrc(renderUrl.toString());
|
|
1499
|
-
}
|
|
1500
|
-
function runBridgeRefresh() {
|
|
1501
|
-
const bridge = bridgeClientRef.current;
|
|
1502
|
-
if (!bridge?.supportsCapability("refresh")) {
|
|
1503
|
-
runFallbackRefresh();
|
|
1504
|
-
return;
|
|
1121
|
+
catch { }
|
|
1122
|
+
boundScrollContainer =
|
|
1123
|
+
iframeDocument.scrollingElement || iframeDocument.body || null;
|
|
1124
|
+
boundScrollContainer?.addEventListener("scroll", handleIframeScroll);
|
|
1125
|
+
try {
|
|
1126
|
+
iframe.contentWindow?.addEventListener("scroll", handleIframeWindowScroll);
|
|
1505
1127
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1128
|
+
catch { }
|
|
1129
|
+
iframeDocument.addEventListener("selectionchange", selecionChangeHandler);
|
|
1130
|
+
iframeDocument.addEventListener("keydown", handleIframeKeyDown);
|
|
1131
|
+
iframeDocument.addEventListener("wheel", handleIframeWheel, {
|
|
1132
|
+
capture: true,
|
|
1133
|
+
passive: false,
|
|
1512
1134
|
});
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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);
|
|
1516
1165
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
if (bridgeClientRef.current?.supportsCapability("refresh")) {
|
|
1520
|
-
runBridgeRefresh();
|
|
1166
|
+
catch {
|
|
1167
|
+
return undefined;
|
|
1521
1168
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
+
}
|
|
1529
1181
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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:")) {
|
|
1551
1235
|
return;
|
|
1552
|
-
const bounds = findBridgeFieldDocumentBounds(pageViewContextRef.current?.bridgeGeometry, fieldsContext.focusedField);
|
|
1553
|
-
const activePageViewContext = pageViewContextRef.current;
|
|
1554
|
-
if (bounds && activePageViewContext) {
|
|
1555
|
-
scrollBridgeBoundsIntoView(activePageViewContext, bounds, latestBridgeScrollRef.current);
|
|
1556
1236
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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;
|
|
1577
1269
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const activePageViewContext = pageViewContextRef.current;
|
|
1584
|
-
if (!activePageViewContext)
|
|
1585
|
-
return;
|
|
1586
|
-
const bounds = findBridgeComponentDocumentBounds(activePageViewContext.bridgeGeometry, editContext.scrollIntoView);
|
|
1587
|
-
if (bounds) {
|
|
1588
|
-
scrollBridgeBoundsIntoView(activePageViewContext, bounds, latestBridgeScrollRef.current);
|
|
1589
|
-
}
|
|
1590
|
-
editContext.setScrollIntoView(undefined);
|
|
1591
|
-
}, [editContext.scrollIntoView, pageViewContext.page]);
|
|
1592
|
-
useEffect(() => {
|
|
1593
|
-
const handleMessage = (message) => {
|
|
1594
|
-
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)
|
|
1595
1275
|
return;
|
|
1596
|
-
|
|
1597
|
-
|
|
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);
|
|
1598
1287
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
|
1601
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);
|
|
1602
1405
|
};
|
|
1603
|
-
window.addEventListener("message", handleMessage);
|
|
1604
|
-
return () => {
|
|
1605
|
-
window.removeEventListener("message", handleMessage);
|
|
1606
|
-
};
|
|
1607
|
-
}, []);
|
|
1608
|
-
useEffect(() => {
|
|
1609
|
-
const iframe = iframeRef.current;
|
|
1610
|
-
if (!iframe)
|
|
1611
|
-
return;
|
|
1612
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
|
+
}
|
|
1613
1417
|
setShowSpinner(false);
|
|
1614
1418
|
applyIframeZoom(iframe, pageViewContextRef.current?.zoom ?? 1, "PageViewerFrame.tsx:handleLoad-zoom");
|
|
1419
|
+
attachListeners();
|
|
1615
1420
|
};
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
iframe
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
if (!iframe || typeof ResizeObserver === "undefined")
|
|
1624
|
-
return;
|
|
1625
|
-
let lastWidth = iframe.clientWidth;
|
|
1626
|
-
let lastHeight = iframe.clientHeight;
|
|
1627
|
-
let geometryTimer = null;
|
|
1628
|
-
let trailingGeometryTimer = null;
|
|
1629
|
-
const requestGeometry = () => {
|
|
1630
|
-
geometryTimer = null;
|
|
1631
|
-
const nextWidth = iframe.clientWidth;
|
|
1632
|
-
const nextHeight = iframe.clientHeight;
|
|
1633
|
-
if (nextWidth === lastWidth && nextHeight === lastHeight)
|
|
1634
|
-
return;
|
|
1635
|
-
lastWidth = nextWidth;
|
|
1636
|
-
lastHeight = nextHeight;
|
|
1637
|
-
bridgeClientRef.current?.sendCommand("queryGeometry", {});
|
|
1638
|
-
};
|
|
1639
|
-
const scheduleGeometryRequest = () => {
|
|
1640
|
-
if (geometryTimer != null) {
|
|
1641
|
-
window.clearTimeout(geometryTimer);
|
|
1642
|
-
}
|
|
1643
|
-
if (trailingGeometryTimer != null) {
|
|
1644
|
-
window.clearTimeout(trailingGeometryTimer);
|
|
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();
|
|
1645
1428
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
requestGeometry();
|
|
1650
|
-
}, ZOOM_TRANSITION_MS + 75);
|
|
1651
|
-
};
|
|
1652
|
-
const resizeObserver = new ResizeObserver(scheduleGeometryRequest);
|
|
1653
|
-
resizeObserver.observe(iframe);
|
|
1429
|
+
iframe.addEventListener("load", handleLoad);
|
|
1430
|
+
}
|
|
1431
|
+
// Cleanup function
|
|
1654
1432
|
return () => {
|
|
1655
|
-
|
|
1656
|
-
if (
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1659
|
-
if (trailingGeometryTimer != null) {
|
|
1660
|
-
window.clearTimeout(trailingGeometryTimer);
|
|
1433
|
+
rebindIframeInteractionsRef.current = null;
|
|
1434
|
+
if (iframe) {
|
|
1435
|
+
iframe.removeEventListener("load", handleLoad);
|
|
1661
1436
|
}
|
|
1437
|
+
detachListeners();
|
|
1662
1438
|
};
|
|
1663
|
-
}, [
|
|
1439
|
+
}, [iframeRef.current]);
|
|
1664
1440
|
useEffect(() => {
|
|
1665
|
-
|
|
1666
|
-
componentIds: editContext.selection,
|
|
1667
|
-
}
|
|
1441
|
+
try {
|
|
1442
|
+
iframeRef.current?.contentWindow?.postMessage({ type: "componentsSelected", componentIds: editContext.selection }, window.location.origin);
|
|
1443
|
+
}
|
|
1444
|
+
catch { }
|
|
1668
1445
|
}, [editContext.selection]);
|
|
1669
|
-
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
layoutKind: editContext.layoutMode,
|
|
1681
|
-
});
|
|
1682
|
-
}, [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
|
+
};
|
|
1683
1457
|
useEffect(() => {
|
|
1684
1458
|
applyIframeZoom(iframeRef.current, zoom, "PageViewerFrame.tsx:zoom-doc");
|
|
1685
|
-
bridgeClientRef.current?.sendCommand("setZoom", { zoom });
|
|
1686
|
-
const geometryTimer = window.setTimeout(() => {
|
|
1687
|
-
bridgeClientRef.current?.sendCommand("queryGeometry", {});
|
|
1688
|
-
}, ZOOM_TRANSITION_MS + 75);
|
|
1689
|
-
return () => {
|
|
1690
|
-
window.clearTimeout(geometryTimer);
|
|
1691
|
-
};
|
|
1692
|
-
}, [zoom]);
|
|
1693
|
-
useEffect(() => {
|
|
1694
|
-
let geometryTimer = null;
|
|
1695
|
-
const sendViewportStateToBridge = (event) => {
|
|
1696
|
-
const detail = event.detail;
|
|
1697
|
-
const nextZoom = detail?.zoom ?? pageViewContextRef.current?.zoom ?? zoom;
|
|
1698
|
-
bridgeClientRef.current?.sendCommand("setZoom", {
|
|
1699
|
-
zoom: nextZoom,
|
|
1700
|
-
});
|
|
1701
|
-
if (geometryTimer != null) {
|
|
1702
|
-
window.clearTimeout(geometryTimer);
|
|
1703
|
-
}
|
|
1704
|
-
geometryTimer = window.setTimeout(() => {
|
|
1705
|
-
geometryTimer = null;
|
|
1706
|
-
bridgeClientRef.current?.sendCommand("queryGeometry", {});
|
|
1707
|
-
}, ZOOM_TRANSITION_MS + 75);
|
|
1708
|
-
};
|
|
1709
|
-
window.addEventListener(DEVICE_CHANGE_EVENT, sendViewportStateToBridge);
|
|
1710
|
-
return () => {
|
|
1711
|
-
if (geometryTimer != null) {
|
|
1712
|
-
window.clearTimeout(geometryTimer);
|
|
1713
|
-
}
|
|
1714
|
-
window.removeEventListener(DEVICE_CHANGE_EVENT, sendViewportStateToBridge);
|
|
1715
|
-
};
|
|
1716
1459
|
}, [zoom]);
|
|
1460
|
+
const scrollHandler = useThrottledCallback(updateScrollPosition, 100);
|
|
1717
1461
|
if (pageViewContext.page?.item && !pageViewContext.page?.item.hasLayout) {
|
|
1718
1462
|
return _jsx(NoLayout, {});
|
|
1719
1463
|
}
|
|
@@ -1729,27 +1473,265 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
|
|
|
1729
1473
|
"px";
|
|
1730
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: {
|
|
1731
1475
|
width: deviceWidth,
|
|
1732
|
-
}, children: [_jsxs("div", { className: "relative h-full w-full overflow-hidden", children: [_jsx("iframe", { ref:
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
}
|
|
1737
|
-
const sendZoomToBridge = () => {
|
|
1738
|
-
bridgeClientRef.current?.sendCommand("setZoom", {
|
|
1739
|
-
zoom: pageViewContextRef.current?.zoom ?? zoom,
|
|
1740
|
-
});
|
|
1741
|
-
};
|
|
1742
|
-
sendZoomToBridge();
|
|
1743
|
-
window.setTimeout(sendZoomToBridge, 100);
|
|
1744
|
-
window.setTimeout(sendZoomToBridge, 500);
|
|
1745
|
-
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) {
|
|
1746
1480
|
setShowSpinner(false);
|
|
1481
|
+
ensureEditorCssGuard(doc, editContext.mode !== "preview");
|
|
1747
1482
|
applyIframeZoom(iframeRef.current, zoom, "PageViewerFrame.tsx:onLoad-zoom");
|
|
1483
|
+
setTimeout(() => {
|
|
1484
|
+
injectSXAScripts(iframeRef.current);
|
|
1485
|
+
}, 1000);
|
|
1486
|
+
requestPageModelBuild(doc);
|
|
1748
1487
|
}
|
|
1749
|
-
} }),
|
|
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 &&
|
|
1750
1489
|
showMiniMap &&
|
|
1751
1490
|
editContext.showMinimap &&
|
|
1752
1491
|
!editContext.isMobile &&
|
|
1753
|
-
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;
|
|
1754
1736
|
}
|
|
1755
1737
|
//# sourceMappingURL=PageViewerFrame.js.map
|