@parhelia/core 0.1.12870 → 0.1.12875

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