@parhelia/core 0.1.12882 → 0.1.12884

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