@alpaca-editor/core 1.0.0

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 (239) hide show
  1. package/.prettierrc +3 -0
  2. package/eslint.config.mjs +4 -0
  3. package/images/bg-shape-black.webp +0 -0
  4. package/package.json +52 -0
  5. package/src/client-components/api.ts +6 -0
  6. package/src/client-components/index.ts +19 -0
  7. package/src/components/ActionButton.tsx +43 -0
  8. package/src/components/Error.tsx +57 -0
  9. package/src/config/config.tsx +737 -0
  10. package/src/config/types.ts +263 -0
  11. package/src/editor/ComponentInfo.tsx +77 -0
  12. package/src/editor/ConfirmationDialog.tsx +103 -0
  13. package/src/editor/ContentTree.tsx +654 -0
  14. package/src/editor/ContextMenu.tsx +155 -0
  15. package/src/editor/Editor.tsx +91 -0
  16. package/src/editor/EditorWarning.tsx +34 -0
  17. package/src/editor/EditorWarnings.tsx +33 -0
  18. package/src/editor/FieldEditorPopup.tsx +65 -0
  19. package/src/editor/FieldHistory.tsx +74 -0
  20. package/src/editor/FieldList.tsx +190 -0
  21. package/src/editor/FieldListField.tsx +387 -0
  22. package/src/editor/FieldListFieldWithFallbacks.tsx +211 -0
  23. package/src/editor/FloatingToolbar.tsx +163 -0
  24. package/src/editor/ImageEditor.tsx +129 -0
  25. package/src/editor/InsertMenu.tsx +332 -0
  26. package/src/editor/ItemInfo.tsx +90 -0
  27. package/src/editor/LinkEditorDialog.tsx +192 -0
  28. package/src/editor/MainLayout.tsx +94 -0
  29. package/src/editor/NewEditorClient.tsx +11 -0
  30. package/src/editor/PictureCropper.tsx +505 -0
  31. package/src/editor/PictureEditor.tsx +206 -0
  32. package/src/editor/PictureEditorDialog.tsx +381 -0
  33. package/src/editor/PublishDialog.ignore +74 -0
  34. package/src/editor/ScrollingContentTree.tsx +47 -0
  35. package/src/editor/Terminal.tsx +215 -0
  36. package/src/editor/Titlebar.tsx +23 -0
  37. package/src/editor/ai/AiPopup.tsx +59 -0
  38. package/src/editor/ai/AiResponseMessage.tsx +82 -0
  39. package/src/editor/ai/AiTerminal.tsx +450 -0
  40. package/src/editor/ai/AiToolCall.tsx +46 -0
  41. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  42. package/src/editor/ai/editorAiContext.ts +18 -0
  43. package/src/editor/client/DialogContext.tsx +49 -0
  44. package/src/editor/client/EditorClient.tsx +1831 -0
  45. package/src/editor/client/GenericDialog.tsx +50 -0
  46. package/src/editor/client/editContext.ts +330 -0
  47. package/src/editor/client/helpers.ts +44 -0
  48. package/src/editor/client/itemsRepository.ts +391 -0
  49. package/src/editor/client/operations.ts +610 -0
  50. package/src/editor/client/pageModelBuilder.ts +182 -0
  51. package/src/editor/commands/commands.ts +23 -0
  52. package/src/editor/commands/componentCommands.tsx +408 -0
  53. package/src/editor/commands/createVersionCommand.ts +33 -0
  54. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  55. package/src/editor/commands/itemCommands.tsx +186 -0
  56. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  57. package/src/editor/commands/undo.ts +39 -0
  58. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  59. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  60. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  61. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  62. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  63. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  64. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  65. package/src/editor/component-designer/PlaceholdersEditor.tsx +183 -0
  66. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  67. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  68. package/src/editor/component-designer/aiContext.ts +23 -0
  69. package/src/editor/componentTreeHelper.tsx +114 -0
  70. package/src/editor/control-center/ControlCenterMenu.tsx +71 -0
  71. package/src/editor/control-center/IndexOverview.tsx +50 -0
  72. package/src/editor/control-center/IndexSettings.tsx +266 -0
  73. package/src/editor/control-center/Status.tsx +7 -0
  74. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  75. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  76. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  77. package/src/editor/editor-warnings/NoWriteAccess.tsx +15 -0
  78. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  79. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  80. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  81. package/src/editor/field-types/DropLinkEditor.tsx +75 -0
  82. package/src/editor/field-types/DropListEditor.tsx +84 -0
  83. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  84. package/src/editor/field-types/InternalLinkFieldEditor.tsx +112 -0
  85. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  86. package/src/editor/field-types/MultiLineText.tsx +63 -0
  87. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  88. package/src/editor/field-types/RawEditor.tsx +53 -0
  89. package/src/editor/field-types/ReactQuill.tsx +580 -0
  90. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  91. package/src/editor/field-types/RichTextEditorComponent.tsx +108 -0
  92. package/src/editor/field-types/SingleLineText.tsx +150 -0
  93. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  94. package/src/editor/fieldTypes.ts +140 -0
  95. package/src/editor/media-selector/AiImageSearch.tsx +186 -0
  96. package/src/editor/media-selector/AiImageSearchPrompt.tsx +95 -0
  97. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  98. package/src/editor/media-selector/Preview.tsx +14 -0
  99. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  100. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  101. package/src/editor/media-selector/UploadZone.tsx +137 -0
  102. package/src/editor/menubar/ActionsMenu.tsx +47 -0
  103. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  104. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  105. package/src/editor/menubar/BrowseHistory.tsx +37 -0
  106. package/src/editor/menubar/ItemLanguageVersion.tsx +52 -0
  107. package/src/editor/menubar/LanguageSelector.tsx +152 -0
  108. package/src/editor/menubar/Menu.tsx +83 -0
  109. package/src/editor/menubar/NavButtons.tsx +74 -0
  110. package/src/editor/menubar/PageSelector.tsx +139 -0
  111. package/src/editor/menubar/PageViewerControls.tsx +99 -0
  112. package/src/editor/menubar/Separator.tsx +12 -0
  113. package/src/editor/menubar/SiteInfo.tsx +53 -0
  114. package/src/editor/menubar/User.tsx +27 -0
  115. package/src/editor/menubar/VersionSelector.tsx +143 -0
  116. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +287 -0
  117. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  118. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +44 -0
  119. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  120. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  121. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  122. package/src/editor/page-editor-chrome/FrameMenu.tsx +263 -0
  123. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  124. package/src/editor/page-editor-chrome/InlineEditor.tsx +147 -0
  125. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  126. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  127. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +119 -0
  128. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +154 -0
  129. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +171 -0
  130. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +233 -0
  131. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  132. package/src/editor/page-viewer/EditorForm.tsx +247 -0
  133. package/src/editor/page-viewer/MiniMap.tsx +351 -0
  134. package/src/editor/page-viewer/PageViewer.tsx +127 -0
  135. package/src/editor/page-viewer/PageViewerFrame.tsx +1030 -0
  136. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  137. package/src/editor/pageModel.ts +191 -0
  138. package/src/editor/picture-shared.tsx +53 -0
  139. package/src/editor/reviews/Comment.tsx +265 -0
  140. package/src/editor/reviews/Comments.tsx +50 -0
  141. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  142. package/src/editor/reviews/Reviews.tsx +280 -0
  143. package/src/editor/reviews/reviewCommands.tsx +47 -0
  144. package/src/editor/reviews/useReviews.tsx +70 -0
  145. package/src/editor/services/aiService.ts +155 -0
  146. package/src/editor/services/componentDesignerService.ts +151 -0
  147. package/src/editor/services/contentService.ts +159 -0
  148. package/src/editor/services/editService.ts +462 -0
  149. package/src/editor/services/indexService.ts +24 -0
  150. package/src/editor/services/reviewsService.ts +45 -0
  151. package/src/editor/services/serviceHelper.ts +95 -0
  152. package/src/editor/services/systemService.ts +5 -0
  153. package/src/editor/services/translationService.ts +21 -0
  154. package/src/editor/services-server/api.ts +150 -0
  155. package/src/editor/services-server/graphQL.ts +106 -0
  156. package/src/editor/sidebar/ComponentPalette.tsx +146 -0
  157. package/src/editor/sidebar/ComponentTree.tsx +512 -0
  158. package/src/editor/sidebar/ComponentTree2.tsxx +490 -0
  159. package/src/editor/sidebar/Debug.tsx +105 -0
  160. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  161. package/src/editor/sidebar/EditHistory.tsx +134 -0
  162. package/src/editor/sidebar/GraphQL.tsx +164 -0
  163. package/src/editor/sidebar/Insert.tsx +35 -0
  164. package/src/editor/sidebar/MainContentTree.tsx +95 -0
  165. package/src/editor/sidebar/Performance.tsx +53 -0
  166. package/src/editor/sidebar/Sessions.tsx +35 -0
  167. package/src/editor/sidebar/Sidebar.tsx +20 -0
  168. package/src/editor/sidebar/SidebarView.tsx +150 -0
  169. package/src/editor/sidebar/Translations.tsx +276 -0
  170. package/src/editor/sidebar/Validation.tsx +102 -0
  171. package/src/editor/sidebar/ViewSelector.tsx +49 -0
  172. package/src/editor/sidebar/Workbox.tsx +209 -0
  173. package/src/editor/ui/CenteredMessage.tsx +7 -0
  174. package/src/editor/ui/CopyToClipboardButton.tsx +23 -0
  175. package/src/editor/ui/DialogButtons.tsx +11 -0
  176. package/src/editor/ui/Icons.tsx +585 -0
  177. package/src/editor/ui/ItemNameDialog.tsx +94 -0
  178. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  179. package/src/editor/ui/ItemSearch.tsx +173 -0
  180. package/src/editor/ui/PerfectTree.tsx +550 -0
  181. package/src/editor/ui/Section.tsx +35 -0
  182. package/src/editor/ui/SimpleIconButton.tsx +43 -0
  183. package/src/editor/ui/SimpleMenu.tsx +48 -0
  184. package/src/editor/ui/SimpleTable.tsx +63 -0
  185. package/src/editor/ui/SimpleTabs.tsx +55 -0
  186. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  187. package/src/editor/ui/Spinner.tsx +7 -0
  188. package/src/editor/ui/Splitter.tsx +247 -0
  189. package/src/editor/ui/StackedPanels.tsx +134 -0
  190. package/src/editor/ui/Toolbar.tsx +7 -0
  191. package/src/editor/utils/id-helper.ts +3 -0
  192. package/src/editor/utils/insertOptions.ts +69 -0
  193. package/src/editor/utils/itemutils.ts +29 -0
  194. package/src/editor/utils/useMemoDebug.ts +28 -0
  195. package/src/editor/utils.ts +435 -0
  196. package/src/editor/views/CompareView.tsx +256 -0
  197. package/src/editor/views/EditView.tsx +27 -0
  198. package/src/editor/views/ItemEditor.tsx +58 -0
  199. package/src/editor/views/SingleEditView.tsx +44 -0
  200. package/src/fonts/Geist-Black.woff2 +0 -0
  201. package/src/fonts/Geist-Bold.woff2 +0 -0
  202. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  203. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  204. package/src/fonts/Geist-Light.woff2 +0 -0
  205. package/src/fonts/Geist-Medium.woff2 +0 -0
  206. package/src/fonts/Geist-Regular.woff2 +0 -0
  207. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  208. package/src/fonts/Geist-Thin.woff2 +0 -0
  209. package/src/fonts/Geist[wght].woff2 +0 -0
  210. package/src/index.ts +7 -0
  211. package/src/page-wizard/PageWizard.tsx +163 -0
  212. package/src/page-wizard/SelectWizard.tsx +109 -0
  213. package/src/page-wizard/WizardSteps.tsx +207 -0
  214. package/src/page-wizard/service.ts +35 -0
  215. package/src/page-wizard/startPageWizardCommand.ts +27 -0
  216. package/src/page-wizard/steps/BuildPageStep.tsx +266 -0
  217. package/src/page-wizard/steps/CollectStep.tsx +233 -0
  218. package/src/page-wizard/steps/ComponentTypesSelector.tsx +443 -0
  219. package/src/page-wizard/steps/Components.tsx +193 -0
  220. package/src/page-wizard/steps/CreatePage.tsx +285 -0
  221. package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +384 -0
  222. package/src/page-wizard/steps/EditButton.tsx +34 -0
  223. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  224. package/src/page-wizard/steps/Generate.tsx +32 -0
  225. package/src/page-wizard/steps/ImagesStep.tsx +318 -0
  226. package/src/page-wizard/steps/LayoutStep.tsx +228 -0
  227. package/src/page-wizard/steps/SelectStep.tsx +256 -0
  228. package/src/page-wizard/steps/schema.ts +180 -0
  229. package/src/page-wizard/steps/usePageCreator.ts +279 -0
  230. package/src/splash-screen/NewPage.tsx +232 -0
  231. package/src/splash-screen/SectionHeadline.tsx +21 -0
  232. package/src/splash-screen/SplashScreen.tsx +156 -0
  233. package/src/tour/Tour.tsx +558 -0
  234. package/src/tour/default-tour.tsx +300 -0
  235. package/src/tour/preview-tour.tsx +127 -0
  236. package/src/types.ts +302 -0
  237. package/styles.css +476 -0
  238. package/tsconfig.build.json +21 -0
  239. package/tsconfig.json +11 -0
@@ -0,0 +1,1030 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { MiniMap } from "./MiniMap";
3
+ import { useEditContext, useEditContextRef } from "../client/editContext";
4
+ import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
5
+ import { PageEditorChrome } from "../page-editor-chrome/PageEditorChrome";
6
+ import { PageViewContext } from "./pageViewContext";
7
+ import morphdom from "morphdom";
8
+ import uuid from "react-uuid";
9
+
10
+ import {
11
+ findComponentRect,
12
+ findFieldElement,
13
+ findNearestComponentId,
14
+ findParentComponentId,
15
+ findParentWithAttribute,
16
+ getAbsolutePosition,
17
+ getFieldDescriptorFromElement,
18
+ findClosestFieldElement,
19
+ } from "../utils";
20
+
21
+ import { getComponentById } from "../componentTreeHelper";
22
+ import { showComponentContextMenu } from "../ContextMenu";
23
+ import { loadFieldButtons } from "../services/editService";
24
+ import { usePathname } from "next/navigation";
25
+ import { EditorWarnings } from "../EditorWarnings";
26
+
27
+ import {
28
+ Component,
29
+ PageSkeleton,
30
+ ComponentSkeleton,
31
+ PlaceholderSkeleton,
32
+ RenderedItemSkeleton,
33
+ ItemDescriptor,
34
+ } from "../pageModel";
35
+ import { NoLayout } from "../page-editor-chrome/NoLayout";
36
+ import { Spinner } from "../ui/Spinner";
37
+ import { DeviceToolbar } from "./DeviceToolbar";
38
+
39
+ declare global {
40
+ interface Window {
41
+ requestRefresh?: (newUri?: string) => boolean;
42
+ }
43
+ }
44
+
45
+ export function PageViewerFrame({
46
+ mode,
47
+ pageViewContext,
48
+ }: {
49
+ mode: "edit" | "compare" | "view";
50
+ pageViewContext?: PageViewContext;
51
+ }) {
52
+ const editContext = useEditContext();
53
+
54
+ const pathname = usePathname();
55
+
56
+ const pageViewContextRef = useRef<PageViewContext | undefined>(undefined);
57
+
58
+ if (!editContext || !pageViewContext) return null;
59
+
60
+ useEffect(() => {
61
+ pageViewContextRef.current = pageViewContext;
62
+ }, [pageViewContext]);
63
+
64
+ const editContextRef = useEditContextRef();
65
+
66
+ const iframeRef = pageViewContext.editorIframeRef;
67
+
68
+ const [showSpinner, setShowSpinner] = useState(false);
69
+ const [scroll, setScroll] = useState(0);
70
+ const [showMiniMap, setShowMiniMap] = useState(false);
71
+
72
+ const zoom = pageViewContext.zoom;
73
+ const blockBlurEventRef = useRef(0);
74
+ const [currentItemDescriptor, setCurrentItemDescriptor] = useState<
75
+ ItemDescriptor | undefined
76
+ >(undefined);
77
+
78
+ const pageItemDescriptor = pageViewContext.pageItemDescriptor;
79
+
80
+ useEffect(() => {
81
+ //Workaround for iframeref updates not propagating to usePageViewContext
82
+ pageViewContext.setWorkaround((x) => !x);
83
+ }, [iframeRef.current]);
84
+
85
+ const updateMiniMapVisibility = useDebouncedCallback(() => {
86
+ if (!iframeRef.current) return;
87
+ const iframe = iframeRef.current;
88
+ const doc = iframe.contentDocument;
89
+ if (!doc?.documentElement) return;
90
+ const scrollContainer = doc.scrollingElement || doc.body;
91
+ if (!scrollContainer) return;
92
+
93
+ const contentHeight = scrollContainer.scrollHeight;
94
+ const clientHeight = iframe.clientHeight;
95
+
96
+ const upperThreshold = clientHeight + 100; // show minimap if content exceeds this height
97
+ const lowerThreshold = clientHeight; // hide minimap if content falls below this
98
+
99
+ if (showMiniMap) {
100
+ if (contentHeight <= lowerThreshold) {
101
+ setShowMiniMap(false);
102
+ }
103
+ } else {
104
+ if (contentHeight > upperThreshold) {
105
+ setShowMiniMap(true);
106
+ }
107
+ }
108
+ }, 100);
109
+
110
+ updateMiniMapVisibility();
111
+
112
+ function buildPageModel(iframeDocument: Document | undefined) {
113
+ if (
114
+ !iframeDocument ||
115
+ !editContextRef.current ||
116
+ !pageViewContextRef.current
117
+ )
118
+ return;
119
+
120
+ const pageItemDescriptor = pageViewContextRef.current?.pageItemDescriptor;
121
+
122
+ if (!pageItemDescriptor) return;
123
+
124
+ const pageItem: RenderedItemSkeleton = {
125
+ ...pageItemDescriptor,
126
+ renderedFieldIds: [],
127
+ };
128
+
129
+ const start = performance.now();
130
+
131
+ const treeWalker = iframeDocument.createTreeWalker(
132
+ iframeDocument.documentElement, // Root node to start traversal
133
+ NodeFilter.SHOW_ELEMENT, // Only show element nodes
134
+ null, // Optional filter function, can be `null`
135
+ );
136
+
137
+ const root: ComponentSkeleton = {
138
+ placeholders: [],
139
+ id: "page",
140
+ type: "Page",
141
+ typeId: "",
142
+ name: "Page",
143
+ items: [pageItem],
144
+ datasourceItem: pageItem,
145
+ renderedDictionaryKeys: [],
146
+ editorFields: {},
147
+ };
148
+
149
+ let currentComponent: ComponentSkeleton | undefined = root;
150
+ let currentPlaceholder: undefined | PlaceholderSkeleton;
151
+
152
+ while (treeWalker.nextNode()) {
153
+ const node = treeWalker.currentNode;
154
+
155
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
156
+
157
+ const element = node as Element;
158
+
159
+ if (currentComponent && element.hasAttribute("data-fieldid")) {
160
+ const fieldId = element.getAttribute("data-fieldid")!;
161
+
162
+ const language =
163
+ element.getAttribute("data-language") ||
164
+ currentComponent.datasourceItem?.language ||
165
+ pageItem.language;
166
+ const version = element.hasAttribute("data-version")
167
+ ? parseInt(element.getAttribute("data-version")!)
168
+ : currentComponent.datasourceItem?.version || pageItem.version;
169
+ const itemId =
170
+ element.getAttribute("data-itemid") ||
171
+ currentComponent.datasourceItem?.id;
172
+
173
+ if (!itemId) continue;
174
+
175
+ let renderedItem = currentComponent.items.find(
176
+ (x) =>
177
+ x.id === itemId && x.language === language && x.version === version,
178
+ );
179
+
180
+ if (!renderedItem) {
181
+ renderedItem = {
182
+ id: itemId,
183
+ language,
184
+ version,
185
+ renderedFieldIds: [],
186
+ };
187
+ currentComponent.items.push(renderedItem);
188
+ }
189
+
190
+ if (renderedItem.renderedFieldIds.indexOf(fieldId) === -1) {
191
+ renderedItem.renderedFieldIds.push(fieldId);
192
+ }
193
+ }
194
+
195
+ if (element.tagName === "SCRIPT") {
196
+ const placeholderStartId = element.getAttribute(
197
+ "data-placeholder-start",
198
+ );
199
+ const placeholderEndId = element.getAttribute("data-placeholder-end");
200
+ const componentStartId = element.getAttribute("data-component-start");
201
+ const componentEndId = element.getAttribute("data-component-end");
202
+ const dictionaryKeyStart = element.getAttribute(
203
+ "data-dictionary-key-start",
204
+ );
205
+ const editorGroup = element.getAttribute("data-editor-group");
206
+
207
+ if (currentComponent && editorGroup) {
208
+ let group = currentComponent.editorFields[editorGroup];
209
+ if (!group) {
210
+ group = {
211
+ addFields: [],
212
+ removeFields: [],
213
+ };
214
+ currentComponent.editorFields[editorGroup] = group;
215
+ }
216
+ const addFields = element.getAttribute("data-add-fields")?.split(",");
217
+ const removeFields = element
218
+ .getAttribute("data-remove-fields")
219
+ ?.split(",");
220
+
221
+ group.addFields = [...group.addFields, ...(addFields || [])];
222
+ group.removeFields = [...group.removeFields, ...(removeFields || [])];
223
+ }
224
+
225
+ if (dictionaryKeyStart) {
226
+ if (!currentComponent) {
227
+ console.error(
228
+ "Dictionary key without component:",
229
+ dictionaryKeyStart,
230
+ );
231
+ } else {
232
+ currentComponent.renderedDictionaryKeys.push(dictionaryKeyStart);
233
+ }
234
+ }
235
+ if (placeholderStartId) {
236
+ if (!currentComponent) {
237
+ console.error(
238
+ "Placeholder start without component:",
239
+ placeholderStartId,
240
+ "Current placeholder",
241
+ currentPlaceholder,
242
+ );
243
+ } else {
244
+ currentPlaceholder = currentComponent.placeholders.find(
245
+ (x) => x.key === placeholderStartId,
246
+ );
247
+
248
+ if (!currentPlaceholder) {
249
+ currentPlaceholder = {
250
+ key: placeholderStartId,
251
+ name:
252
+ placeholderStartId.indexOf(currentComponent.id + "_") === 0
253
+ ? placeholderStartId.substring(
254
+ currentComponent.id.length + 1,
255
+ )
256
+ : placeholderStartId,
257
+ description: "",
258
+ components: [],
259
+ parentComponent: currentComponent,
260
+ };
261
+
262
+ currentComponent.placeholders.push(currentPlaceholder);
263
+ }
264
+ currentComponent = undefined;
265
+ }
266
+ }
267
+
268
+ if (placeholderEndId) {
269
+ currentComponent = currentPlaceholder?.parentComponent;
270
+ if (currentPlaceholder?.key !== placeholderEndId) {
271
+ console.error(
272
+ "Placeholder end does not match start",
273
+ currentPlaceholder,
274
+ placeholderEndId,
275
+ );
276
+ }
277
+ currentPlaceholder = undefined;
278
+ }
279
+
280
+ if (componentStartId) {
281
+ if (!currentPlaceholder) {
282
+ if (!currentComponent) {
283
+ console.error(
284
+ "Component start without parent component:",
285
+ componentStartId,
286
+ );
287
+ continue;
288
+ }
289
+
290
+ currentPlaceholder = {
291
+ key: "implicit_" + componentStartId,
292
+ name: "no-placeholder",
293
+ description: "",
294
+ components: [],
295
+ parentComponent: currentComponent,
296
+ };
297
+ if (!currentComponent) {
298
+ console.error(
299
+ "Component start without placeholder or parent component",
300
+ );
301
+ } else {
302
+ currentComponent.placeholders.push(currentPlaceholder);
303
+ }
304
+ }
305
+ if (currentPlaceholder) {
306
+ const language =
307
+ element.getAttribute("data-language") || pageItem.language;
308
+ const version = element.hasAttribute("data-version")
309
+ ? parseInt(element.getAttribute("data-version")!)
310
+ : pageItem.version;
311
+
312
+ const itemId = element.getAttribute("data-itemid");
313
+
314
+ const datasourceItem = itemId
315
+ ? {
316
+ id: itemId,
317
+ language,
318
+ version,
319
+ renderedFieldIds: [],
320
+ }
321
+ : undefined;
322
+
323
+ const layoutId = element.getAttribute("data-layoutid");
324
+
325
+ currentComponent = currentPlaceholder.components.find(
326
+ (x) => x.id === componentStartId,
327
+ );
328
+
329
+ if (!currentComponent) {
330
+ currentComponent = {
331
+ id: componentStartId,
332
+ name: "",
333
+ type: element.getAttribute("data-type") || "",
334
+ typeId: element.getAttribute("data-typeid") || "",
335
+ items: datasourceItem ? [datasourceItem] : [],
336
+ placeholders: [],
337
+ parentPlaceholder: currentPlaceholder,
338
+ renderedDictionaryKeys: [],
339
+ datasourceItem,
340
+ layoutId: layoutId || undefined,
341
+ editorFields: {},
342
+ };
343
+
344
+ currentPlaceholder.components.push(currentComponent);
345
+ }
346
+ currentPlaceholder = undefined;
347
+ }
348
+ }
349
+
350
+ if (componentEndId) {
351
+ if (!currentComponent || currentComponent.id !== componentEndId) {
352
+ console.error(
353
+ "Component end does not match start",
354
+ currentComponent,
355
+ componentEndId,
356
+ );
357
+
358
+ // Placeholder closed implicitly
359
+ if (currentPlaceholder?.parentComponent.id === componentEndId) {
360
+ currentComponent = currentPlaceholder.parentComponent;
361
+ currentPlaceholder = undefined;
362
+ }
363
+ }
364
+ currentPlaceholder = currentComponent?.parentPlaceholder;
365
+
366
+ if (currentPlaceholder?.key.startsWith("implicit_")) {
367
+ currentComponent = currentPlaceholder.parentComponent;
368
+ } else currentComponent = undefined;
369
+ }
370
+ }
371
+ }
372
+
373
+ const page: PageSkeleton = {
374
+ rootComponent: root,
375
+ item: pageItem,
376
+ editRevision: editContextRef.current?.revision ?? "",
377
+ };
378
+ const time = performance.now() - start;
379
+
380
+ console.log("PAGE MODEL SKELETON", page, time);
381
+ pageViewContextRef.current?.setPageSkeleton(page);
382
+ }
383
+
384
+ const buildPageModelThrottled = useThrottledCallback(buildPageModel, 1000);
385
+
386
+ const [iframeSrc, setIframeSrc] = useState<string>();
387
+
388
+ useEffect(() => {
389
+ const isHeadless = pageViewContext.isHeadless;
390
+ if (!pageItemDescriptor || isHeadless === undefined) return;
391
+
392
+ let renderUrl: URL;
393
+ if (!isHeadless) {
394
+ // Sitecore MVC rendering
395
+ renderUrl = new URL(window.location.href);
396
+ renderUrl.pathname = "/";
397
+
398
+ renderUrl.searchParams.set("sc_itemid", pageItemDescriptor.id);
399
+ renderUrl.searchParams.set("sc_lang", pageItemDescriptor.language);
400
+ renderUrl.searchParams.set(
401
+ "sc_version",
402
+ pageItemDescriptor.version.toString(),
403
+ );
404
+ renderUrl.searchParams.set(
405
+ "sc_mode",
406
+ editContext.previewMode ? "preview" : "edit",
407
+ );
408
+ renderUrl.searchParams.set("alpaca_editor", "1");
409
+ renderUrl.searchParams.set("sc_database", "master");
410
+ //renderUrl.searchParams.set(
411
+ // "sc_site",
412
+ // editContext.site?.name || "website"
413
+ // );
414
+ } else {
415
+ // Headless rendering
416
+ console.log("Headless rendering", pageItemDescriptor);
417
+ renderUrl = new URL(window.location.href);
418
+ if (editContext.configuration.services.renderService.path)
419
+ renderUrl.pathname =
420
+ editContext.configuration.services.renderService.path;
421
+
422
+ renderUrl.searchParams.set("itemid", pageItemDescriptor.id);
423
+ renderUrl.searchParams.set("lang", pageItemDescriptor.language);
424
+ renderUrl.searchParams.set(
425
+ "version",
426
+ pageItemDescriptor.version.toString(),
427
+ );
428
+
429
+ renderUrl.searchParams.set(
430
+ "site",
431
+ editContext.pageView.site?.name || "website",
432
+ );
433
+ renderUrl.searchParams.set(
434
+ "mode",
435
+ editContext.previewMode ? "preview" : "edit",
436
+ );
437
+ }
438
+
439
+ renderUrl.searchParams.set("edit_rev", editContext.revision ?? uuid());
440
+
441
+ if (iframeRef.current?.contentWindow?.requestRefresh) {
442
+ console.log("Integration - requesting refresh");
443
+ iframeRef.current?.contentWindow.requestRefresh(renderUrl.toString());
444
+ } else {
445
+ setShowSpinner(true);
446
+ console.log("No integration - reloading frame");
447
+ if (isHeadless) {
448
+ console.log("Setting iframe src", renderUrl.toString());
449
+ setIframeSrc(renderUrl.toString());
450
+ } else {
451
+ const initialLoad =
452
+ currentItemDescriptor?.id !== pageItemDescriptor.id ||
453
+ currentItemDescriptor?.language !== pageItemDescriptor.language ||
454
+ currentItemDescriptor?.version !== pageItemDescriptor.version;
455
+
456
+ loadContent(renderUrl.toString(), initialLoad);
457
+ }
458
+ }
459
+
460
+ setCurrentItemDescriptor(pageItemDescriptor);
461
+ }, [
462
+ pathname,
463
+ editContext.revision,
464
+ pageItemDescriptor,
465
+ pageViewContext.isHeadless,
466
+ editContext.previewMode,
467
+ ]);
468
+
469
+ useEffect(() => {
470
+ if (editContext.focusedField) {
471
+ if (
472
+ editContext.selection.length > 0 &&
473
+ editContext.focusedField.item.id !== editContext.selection[0]
474
+ )
475
+ return;
476
+
477
+ const fieldElement = findFieldElement(
478
+ iframeRef.current!,
479
+ editContext.focusedField,
480
+ );
481
+ if (fieldElement) {
482
+ const rect = getAbsolutePosition(
483
+ fieldElement as HTMLElement,
484
+ iframeRef.current!,
485
+ );
486
+
487
+ const scrollTop = iframeRef.current?.contentWindow?.scrollY || 0;
488
+ const iframeHeight = iframeRef.current?.getBoundingClientRect().height;
489
+
490
+ const isInViewport =
491
+ rect.y >= scrollTop && rect.y <= scrollTop + (iframeHeight || 0);
492
+
493
+ if (!isInViewport) {
494
+ iframeRef.current?.contentWindow?.scrollTo({
495
+ top: rect.y,
496
+ behavior: "smooth",
497
+ });
498
+ }
499
+ }
500
+ } else {
501
+ if (editContext.selection.length > 0) {
502
+ const lastSelectedComponent = getComponentById(
503
+ editContext.selection[editContext.selection.length - 1]!,
504
+ pageViewContextRef.current!.page!,
505
+ );
506
+ if (lastSelectedComponent) {
507
+ editContext.setScrollIntoView(lastSelectedComponent.id);
508
+ }
509
+ }
510
+ }
511
+ }, [editContext.focusedField, editContext.selection]);
512
+
513
+ const loadContent = async (href: string, initialLoad: boolean) => {
514
+ console.log("Loading content:", href);
515
+ const start = performance.now();
516
+ if (!href) return;
517
+ const content = await fetch(href);
518
+ const text = await content.text();
519
+ console.log("Content loaded in " + (performance.now() - start) + " ms");
520
+ const doc = iframeRef.current?.contentDocument;
521
+
522
+ if (doc) {
523
+ if (initialLoad) {
524
+ doc.open();
525
+ doc.write(text);
526
+ doc.close();
527
+ } else {
528
+ const parser = new DOMParser();
529
+ const newDoc = parser.parseFromString(text, "text/html");
530
+ morphdom(doc.documentElement, newDoc.documentElement);
531
+ setShowSpinner(false);
532
+ }
533
+
534
+ if (mode === "edit" && !editContext.previewMode) injectEditorCSS(doc);
535
+
536
+ buildPageModel(doc);
537
+ }
538
+ };
539
+
540
+ useEffect(() => {
541
+ if (
542
+ !editContext.scrollIntoView ||
543
+ !iframeRef.current?.contentDocument?.documentElement
544
+ )
545
+ return;
546
+
547
+ const rect = findComponentRect(
548
+ iframeRef.current!,
549
+ editContext.scrollIntoView,
550
+ true,
551
+ );
552
+
553
+ if (!rect) return;
554
+
555
+ // Check if element is already in viewport
556
+ const iframeHeight = iframeRef.current.getBoundingClientRect().height;
557
+
558
+ const scrollTop = iframeRef.current?.contentWindow?.scrollY || 0;
559
+ const elementTop = rect.rect.y;
560
+
561
+ const isInViewport =
562
+ elementTop >= scrollTop && elementTop <= scrollTop + iframeHeight;
563
+
564
+ // If already in viewport, no need to scroll
565
+ if (isInViewport) {
566
+ editContext.setScrollIntoView(undefined);
567
+ return;
568
+ }
569
+
570
+ const scrollPosition =
571
+ rect.rect.y - iframeHeight / 2 + rect.rect.height / 2;
572
+
573
+ iframeRef.current?.contentWindow?.scrollTo({
574
+ top: scrollPosition,
575
+ behavior: "smooth",
576
+ });
577
+
578
+ editContext.setScrollIntoView(undefined);
579
+ }, [editContext.scrollIntoView, pageViewContext.page]);
580
+
581
+ useEffect(() => {
582
+ const handleMessage = (message: MessageEvent) => {
583
+ if (message.data.type === "editor-exitFullscreen") {
584
+ editContext.pageView.setFullscreen(false);
585
+ }
586
+ if (message.data.type === "editor-timings") {
587
+ editContext.setTimings(message.data.timings);
588
+ }
589
+ };
590
+ window.addEventListener("message", handleMessage);
591
+
592
+ return () => {
593
+ window.removeEventListener("message", handleMessage);
594
+ };
595
+ }, []);
596
+
597
+ const selecionChangeHandler = useThrottledCallback(() => {
598
+ const sel = iframeRef.current?.contentDocument?.getSelection();
599
+
600
+ if (sel && sel.rangeCount > 0) {
601
+ // Find the field element containing the selection
602
+ const fieldElement = findClosestFieldElement(sel.anchorNode);
603
+
604
+ if (!fieldElement) return;
605
+
606
+ const fieldId = fieldElement.getAttribute("data-fieldid");
607
+ if (!fieldId) return;
608
+
609
+ const range = sel.getRangeAt(0);
610
+
611
+ editContextRef.current?.setFocusedField(
612
+ {
613
+ fieldId: fieldId,
614
+ item: {
615
+ id: fieldElement.getAttribute("data-itemid") || "",
616
+ language: fieldElement.getAttribute("data-language") || "",
617
+ version: parseInt(fieldElement.getAttribute("data-version") || "0"),
618
+ },
619
+ },
620
+ true,
621
+ );
622
+
623
+ // Compute the global offsets relative to the field element.
624
+ const globalStartOffset = getGlobalTextOffset(
625
+ fieldElement,
626
+ range.startContainer,
627
+ range.startOffset,
628
+ );
629
+ const globalEndOffset = getGlobalTextOffset(
630
+ fieldElement,
631
+ range.endContainer,
632
+ range.endOffset,
633
+ );
634
+
635
+ editContextRef.current?.setSelectedRange({
636
+ fieldId: fieldId,
637
+ startOffset: globalStartOffset,
638
+ endOffset: globalEndOffset,
639
+ text: range.toString(),
640
+ });
641
+ }
642
+ }, 100);
643
+
644
+ useEffect(() => {
645
+ const iframe = iframeRef.current;
646
+ if (!iframe) return;
647
+
648
+ const handleIframeMouseDown = async (event: any) => {
649
+ const target = event.target;
650
+
651
+ if (editContextRef.current?.isRefreshing) return;
652
+
653
+ const componentId = findParentComponentId(target);
654
+ editContextRef.current?.setCurrentOverlay(undefined);
655
+
656
+ if (componentId) {
657
+ if (event.ctrlKey) {
658
+ const currentSelection = editContextRef.current?.selection || [];
659
+ if (currentSelection.indexOf(componentId) === -1)
660
+ editContextRef.current?.select([...currentSelection, componentId]);
661
+ } else {
662
+ editContextRef.current?.select([componentId]);
663
+ }
664
+
665
+ // if (mode !== "edit") {
666
+ //editContextRef.current?.setScrollIntoView(componentId);
667
+ //}
668
+ } else editContextRef.current?.select([]);
669
+
670
+ if (
671
+ mode === "edit" &&
672
+ pageViewContextRef.current?.page?.item.canWriteItem
673
+ ) {
674
+ const fieldElement = findParentWithAttribute(target, "data-fieldid");
675
+ if (fieldElement?.hasAttribute("data-itemid")) {
676
+ const hasLock = await editContextRef.current?.setFocusedField(
677
+ getFieldDescriptorFromElement(fieldElement),
678
+ true,
679
+ );
680
+ blockBlurEventRef.current = Date.now() + 500;
681
+
682
+ if (hasLock) {
683
+ editContextRef.current?.setInlineEditingFieldElement(fieldElement);
684
+ }
685
+ }
686
+ }
687
+ //Forward events so primereact overlays can close
688
+ const clickEvent = new MouseEvent("click", {
689
+ view: window,
690
+ bubbles: true,
691
+ cancelable: true,
692
+ clientX: event.clientX,
693
+ clientY: event.clientY,
694
+ });
695
+
696
+ document.dispatchEvent(clickEvent);
697
+ };
698
+
699
+ const handleIframeClick = async (event: any) => {
700
+ const target = event.target;
701
+ // Check if the click target is a link (anchor tag)
702
+ const anchor =
703
+ target.tagName.toLowerCase() === "a" ? target : target.closest("a");
704
+ if (anchor) {
705
+ const href = (anchor as HTMLAnchorElement).href;
706
+
707
+ // Block only navigation links, allow anchor links and javascript links
708
+ if (href && !href.startsWith("#") && !href.startsWith("javascript:")) {
709
+ event.preventDefault();
710
+ console.log("Navigation link blocked:", href);
711
+ }
712
+ }
713
+ };
714
+
715
+ const handleContextMenu = async (event: MouseEvent) => {
716
+ if (editContextRef.current?.isRefreshing) return;
717
+ const target = event.target;
718
+ if (!target) return;
719
+
720
+ if (event.ctrlKey) return;
721
+
722
+ event.preventDefault();
723
+ event.stopPropagation();
724
+
725
+ const componentId = findNearestComponentId(target as HTMLElement);
726
+
727
+ if (componentId) {
728
+ if (!editContextRef.current?.selection.includes(componentId)) {
729
+ console.log("Selecting component CONTEXT MENU", componentId);
730
+ editContextRef.current!.select([componentId]);
731
+ }
732
+ }
733
+
734
+ const fieldElement = findParentWithAttribute(
735
+ target as HTMLElement,
736
+ "data-fieldid",
737
+ );
738
+
739
+ const selectedComponents = editContextRef.current?.selection
740
+ .map((id) => getComponentById(id, pageViewContextRef.current!.page!))
741
+ .filter((x) => x) as Component[];
742
+
743
+ const iframeRect = iframe.getBoundingClientRect();
744
+ const adjustedEvent = new MouseEvent("contextmenu", {
745
+ bubbles: true,
746
+ cancelable: true,
747
+ shiftKey: event.shiftKey,
748
+ altKey: event.altKey,
749
+ ctrlKey: event.ctrlKey,
750
+ view: window,
751
+ clientX: event.clientX + iframeRect.x,
752
+ clientY: event.clientY + iframeRect.y,
753
+ });
754
+
755
+ const field = fieldElement
756
+ ? getFieldDescriptorFromElement(fieldElement)
757
+ : undefined;
758
+
759
+ const fieldButtons = field ? await loadFieldButtons(field) : [];
760
+
761
+ if (
762
+ showComponentContextMenu(
763
+ selectedComponents,
764
+ editContextRef.current!,
765
+ field,
766
+ fieldButtons,
767
+ )(adjustedEvent)
768
+ ) {
769
+ }
770
+ };
771
+
772
+ const handleLoad = () => {
773
+ setShowSpinner(false);
774
+ const iframeDocument =
775
+ iframe!.contentDocument || iframe!.contentWindow?.document;
776
+ if (iframeDocument) {
777
+ iframeDocument.documentElement.addEventListener(
778
+ "mousedown",
779
+ handleIframeMouseDown,
780
+ );
781
+ iframeDocument.documentElement.addEventListener(
782
+ "click",
783
+ handleIframeClick,
784
+ );
785
+ iframe.contentWindow?.addEventListener(
786
+ "contextmenu",
787
+ handleContextMenu,
788
+ true,
789
+ );
790
+
791
+ const scrollContainer =
792
+ iframe.contentWindow?.document.scrollingElement ||
793
+ iframe.contentWindow?.document.body;
794
+ scrollContainer?.addEventListener("scroll", () => {
795
+ const scrollTop = scrollContainer.scrollTop || 0;
796
+ scrollHandler(scrollTop);
797
+ });
798
+ iframe.contentWindow?.addEventListener("scroll", () => {
799
+ const scrollTop = scrollContainer?.scrollTop || 0;
800
+ scrollHandler(scrollTop);
801
+ });
802
+
803
+ iframeDocument.addEventListener(
804
+ "selectionchange",
805
+ selecionChangeHandler,
806
+ );
807
+
808
+ iframeDocument.addEventListener("keydown", (event: KeyboardEvent) => {
809
+ editContextRef.current?.handleKeyDown(event);
810
+ });
811
+
812
+ iframeDocument.documentElement?.addEventListener(
813
+ "blur",
814
+ () => {
815
+ // Block blur event if it was triggered by clicking on a field
816
+ if (blockBlurEventRef.current < Date.now()) {
817
+ //editContext.setFocusedField(undefined, false);
818
+ editContext.setInlineEditingFieldElement(undefined);
819
+ } else {
820
+ blockBlurEventRef.current = 0;
821
+ }
822
+ },
823
+ true,
824
+ );
825
+
826
+ const observer = new MutationObserver((records) => {
827
+ // Ignore all text field edits
828
+ if (
829
+ records.some(
830
+ (x) =>
831
+ !(
832
+ x &&
833
+ "getAttribute" in x.target &&
834
+ (x.target as Element).getAttribute("data-fieldid") &&
835
+ (x.target as Element).getAttribute("data-itemid")
836
+ ),
837
+ )
838
+ ) {
839
+ buildPageModelThrottled(iframeDocument);
840
+ }
841
+ });
842
+
843
+ observer.observe(iframeDocument!, {
844
+ childList: true, // observe direct children changes
845
+ subtree: true, // observe all descendants changes
846
+ characterData: false, // observe text changes
847
+
848
+ //attributes: true, // observe attribute changes (like style or class)
849
+ });
850
+
851
+ buildPageModel(iframeDocument);
852
+ }
853
+ };
854
+
855
+ if (iframe) {
856
+ iframe.addEventListener("load", handleLoad);
857
+ }
858
+
859
+ // Cleanup function
860
+ return () => {
861
+ if (iframe) {
862
+ iframe.removeEventListener("load", handleLoad);
863
+
864
+ const iframeDocument =
865
+ iframe.contentDocument || iframe.contentWindow?.document;
866
+ if (iframeDocument) {
867
+ iframeDocument.documentElement.removeEventListener(
868
+ "click",
869
+ handleIframeMouseDown,
870
+ );
871
+ iframeDocument.documentElement.removeEventListener(
872
+ "contextmenu",
873
+ handleContextMenu,
874
+ );
875
+ }
876
+ }
877
+ };
878
+ }, [iframeRef.current]);
879
+
880
+ useEffect(() => {
881
+ iframeRef.current?.contentWindow?.postMessage(
882
+ { type: "componentsSelected", componentIds: editContext.selection },
883
+ "*",
884
+ );
885
+ }, [editContext.selection]);
886
+
887
+ const updateScrollPosition = (e: number) => {
888
+ setScroll(e);
889
+ if (mode === "edit") pageViewContextRef.current?.setScroll(e);
890
+ };
891
+
892
+ useEffect(() => {
893
+ if (!iframeRef.current) return;
894
+ const body = iframeRef.current.contentWindow?.document.documentElement;
895
+ if (!body) return;
896
+ body.style.transform = `scale(${zoom})`;
897
+ body.style.transformOrigin = "top center";
898
+ body.style.overflow = "auto";
899
+ //body.style.height = `${body.scrollHeight / zoom}px`;
900
+ }, [zoom]);
901
+
902
+ const scrollHandler = useThrottledCallback(updateScrollPosition, 100);
903
+
904
+ if (pageViewContext.page?.item && !pageViewContext.page?.item.hasLayout) {
905
+ return <NoLayout />;
906
+ }
907
+
908
+ const deviceHeight =
909
+ pageViewContext.device === "desktop" || !pageViewContext.deviceHeight
910
+ ? "100%"
911
+ : pageViewContext.deviceHeight || 640;
912
+
913
+ return (
914
+ <div className="relative flex h-full w-full flex-col items-center bg-slate-100 select-none">
915
+ {!editContext.pageView.fullscreen && (
916
+ <EditorWarnings item={pageViewContext.page?.item} />
917
+ )}
918
+ {pageViewContext.device !== "desktop" && (
919
+ <DeviceToolbar
920
+ pageViewContext={pageViewContext}
921
+ configuration={editContext.configuration}
922
+ />
923
+ )}
924
+ <div
925
+ className="relative flex flex-1 select-none"
926
+ style={{
927
+ width:
928
+ pageViewContext.device === "desktop" ||
929
+ pageViewContext.device === "Responsive"
930
+ ? "100%"
931
+ : (pageViewContext.deviceWidth || 640) +
932
+ editContext.configuration.outline.width +
933
+ "px",
934
+ }}
935
+ >
936
+ <div className="relative h-full w-full">
937
+ <iframe
938
+ ref={iframeRef}
939
+ className="page-iframe h-full w-full bg-white"
940
+ style={{ height: deviceHeight }}
941
+ src={iframeSrc}
942
+ data-testid="pageEditoriframe"
943
+ />
944
+
945
+ {iframeRef.current && (
946
+ <PageEditorChrome
947
+ iframe={iframeRef.current}
948
+ mode={mode || "edit"}
949
+ pageViewContext={pageViewContext}
950
+ />
951
+ )}
952
+ {pageViewContext.deviceHeight && pageViewContext.device && (
953
+ <div className="relative z-40 h-full w-full bg-slate-100"></div>
954
+ )}
955
+ </div>
956
+ {!editContext.pageView.fullscreen && showMiniMap && (
957
+ <MiniMap
958
+ mode={mode}
959
+ scroll={scroll}
960
+ mainViewIframeRef={iframeRef}
961
+ pageViewContext={pageViewContext}
962
+ deviceHeight={
963
+ pageViewContext.device === "Desktop"
964
+ ? undefined
965
+ : pageViewContext.deviceHeight
966
+ }
967
+ />
968
+ )}
969
+ {showSpinner && (
970
+ <>
971
+ <div className="absolute top-0 left-0 h-full w-full bg-slate-100/50"></div>
972
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
973
+ <Spinner />
974
+ </div>
975
+ </>
976
+ )}
977
+ </div>
978
+ </div>
979
+ );
980
+ }
981
+
982
+ export function injectEditorCSS(iframeDocument: Document) {
983
+ if (!iframeDocument) return;
984
+ const style = iframeDocument.createElement("style");
985
+ style.textContent = `[contentEditable]:empty:before {
986
+ content: attr(placeholder);
987
+ opacity: 0.6;
988
+ }
989
+
990
+ [contenteditable] {
991
+ outline: 0px solid transparent;
992
+ }
993
+ `;
994
+
995
+ if (iframeDocument && iframeDocument.head) {
996
+ iframeDocument.head.appendChild(style);
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * Calculates the global offset of a given target node and its local offset,
1002
+ * relative to the container's complete text content.
1003
+ *
1004
+ * @param container - The container element that holds the text nodes.
1005
+ * @param targetNode - The text node where the selection starts or ends.
1006
+ * @param localOffset - The offset within the target text node.
1007
+ * @returns The computed global offset.
1008
+ */
1009
+ function getGlobalTextOffset(
1010
+ container: Node,
1011
+ targetNode: Node,
1012
+ localOffset: number,
1013
+ ): number {
1014
+ let globalOffset = 0;
1015
+ const walker = document.createTreeWalker(
1016
+ container,
1017
+ NodeFilter.SHOW_TEXT,
1018
+ null,
1019
+ );
1020
+ while (walker.nextNode()) {
1021
+ const currentNode = walker.currentNode;
1022
+ // If we've reached the target node, add the local offset and return.
1023
+ if (currentNode === targetNode) {
1024
+ return globalOffset + localOffset;
1025
+ }
1026
+ // Otherwise, add the length of this text node.
1027
+ globalOffset += (currentNode.textContent || "").length;
1028
+ }
1029
+ return globalOffset;
1030
+ }