@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,247 @@
1
+ import { useEditContext } from "../client/editContext";
2
+ import { getComponentById } from "../componentTreeHelper";
3
+
4
+ import { FieldList, ItemFields } from "../FieldList";
5
+ import { ItemInfo } from "../ItemInfo";
6
+ import { Insert } from "../sidebar/Insert";
7
+ import { useEffect, useState } from "react";
8
+
9
+ import { SimpleIconButton } from "../ui/SimpleIconButton";
10
+ import { Component, Field, RenderedItem } from "../pageModel";
11
+ import { Spinner } from "../ui/Spinner";
12
+ import { PageViewContext } from "./pageViewContext";
13
+ import { SimpleTabs, Tab } from "../ui/SimpleTabs";
14
+
15
+ export function EditorForm({
16
+ pageViewContext,
17
+ readonly,
18
+ }: {
19
+ pageViewContext?: PageViewContext;
20
+ readonly?: boolean;
21
+ }) {
22
+ const editContext = useEditContext()!;
23
+ if (!pageViewContext) pageViewContext = editContext.pageView;
24
+
25
+ const setInsertMode = editContext.setInsertMode;
26
+ const insertMode = editContext.insertMode;
27
+ const [activeTabKey, setActiveTabKey] = useState("content");
28
+ const [item, setItem] = useState<RenderedItem>();
29
+ const [component, setComponent] = useState<Component>();
30
+
31
+ const toggleInsertMode = () => {
32
+ if (insertMode) {
33
+ setInsertMode(false);
34
+ } else {
35
+ // if (editContext.selection.length > 0)
36
+ // editContext.setSelectedForInsertion(editContext.selection[0]);
37
+ setInsertMode(true);
38
+ }
39
+ };
40
+
41
+ useEffect(() => {
42
+ if (editContext.selectedForInsertion) setInsertMode(true);
43
+ }, [editContext.selectedForInsertion]);
44
+
45
+ let isShared = false;
46
+
47
+ useEffect(() => {
48
+ if (!pageViewContext.page) return;
49
+ let newSelectedItem;
50
+
51
+ if (editContext.selection.length === 1) {
52
+ const component = getComponentById(
53
+ editContext.selection[0]!,
54
+ pageViewContext.page,
55
+ );
56
+ setComponent(component);
57
+ newSelectedItem = component?.datasourceItem;
58
+ if (
59
+ component?.datasourceItem &&
60
+ component?.datasourceItem?.id !== component?.id
61
+ )
62
+ isShared = true;
63
+ } else {
64
+ newSelectedItem = pageViewContext.page.rootComponent.datasourceItem;
65
+ setComponent(pageViewContext.page.rootComponent);
66
+ }
67
+
68
+ // if (
69
+ // newSelectedItem?.id !== item?.id ||
70
+ // newSelectedItem?.language !== item?.language ||
71
+ // newSelectedItem?.version !== item?.version
72
+ // ) {
73
+ // setInsertMode(false);
74
+ // }
75
+
76
+ setItem(
77
+ newSelectedItem || pageViewContext.page.rootComponent.datasourceItem,
78
+ );
79
+ }, [
80
+ editContext.selection,
81
+ pageViewContext.page,
82
+ pageViewContext.pageItemDescriptor,
83
+ ]);
84
+
85
+ if (!item)
86
+ return (
87
+ <div className="grid h-full w-full items-center justify-center">
88
+ <Spinner />
89
+ </div>
90
+ );
91
+
92
+ const validators =
93
+ editContext.validationResult?.find(
94
+ (x) =>
95
+ x.item.id === item?.id &&
96
+ x.item.language === item?.language &&
97
+ x.item.version === item?.version,
98
+ )?.results || [];
99
+
100
+ let designFields = Object.values(item?.fields).filter(
101
+ (x) => x.section === "Design" || x.section === "Rendering",
102
+ );
103
+
104
+ const editorFields = component?.editorFields;
105
+
106
+ if (editorFields && editorFields["Design"]) {
107
+ editorFields["Design"]?.addFields.forEach((fieldName) => {
108
+ const field = item?.fields.find((x) => x.name === fieldName);
109
+ if (field) designFields.push(field);
110
+ });
111
+ editorFields["Design"]?.removeFields.forEach((fieldName) => {
112
+ designFields = designFields.filter((x) => x.name !== fieldName);
113
+ });
114
+ }
115
+
116
+ const itemFields: ItemFields[] = [];
117
+
118
+ const contentFields = Object.values(item?.fields).filter(
119
+ (x) =>
120
+ ((editorFields && editorFields["Content"]?.addFields.includes(x.name!)) ||
121
+ item.renderedFieldIds.indexOf(x.id) >= 0) &&
122
+ !(
123
+ editorFields && editorFields["Content"]?.removeFields.includes(x.name!)
124
+ ),
125
+ );
126
+ if (contentFields.length > 0) itemFields.push({ fields: contentFields });
127
+
128
+ component?.items
129
+ .filter((x) => x.id !== item.id)
130
+ .forEach((i) => {
131
+ const fields: Field[] = [];
132
+ i.renderedFieldIds.forEach((id) => {
133
+ const field = Object.values(i?.fields).find((x) => x.id === id);
134
+ if (field) fields.push(field);
135
+ });
136
+ if (fields.length > 0) itemFields.push({ headline: i.name, fields });
137
+ });
138
+
139
+ const getFieldsTab = (fields: ItemFields[], headline: string, id: string) => {
140
+ return {
141
+ label: headline,
142
+ content: (
143
+ <div className="relative h-full">
144
+ <div className="absolute inset-0 overflow-auto px-3">
145
+ <FieldList
146
+ fields={fields}
147
+ validators={validators}
148
+ simplified={true}
149
+ readonly={readonly}
150
+ />
151
+ </div>
152
+ </div>
153
+ ),
154
+ id,
155
+ };
156
+ };
157
+
158
+ const tabPanels: Tab[] = [];
159
+ if (itemFields.length > 0)
160
+ tabPanels.push(getFieldsTab(itemFields, "Content", "content"));
161
+
162
+ //TODO: generalize this
163
+ if (editorFields) {
164
+ Object.keys(editorFields).forEach((key) => {
165
+ if (key !== "Content" && key !== "Design") {
166
+ const fields = item?.fields.filter((x) =>
167
+ editorFields[key]?.addFields.includes(x.name!),
168
+ );
169
+ if (fields?.length)
170
+ tabPanels.push(getFieldsTab([{ fields }], key, key));
171
+ }
172
+ });
173
+ }
174
+
175
+ if (designFields?.length)
176
+ tabPanels.push(
177
+ getFieldsTab([{ fields: designFields }], "Design", "design"),
178
+ );
179
+
180
+ tabPanels.push({
181
+ label: "Advanced",
182
+ content: (
183
+ <div className="relative h-full">
184
+ <div className="absolute inset-0 overflow-auto px-3">
185
+ <ItemInfo item={item} />
186
+ <FieldList
187
+ fields={[{ fields: item.fields }]}
188
+ validators={validators}
189
+ showStandardFieldsEnabled={true}
190
+ readonly={readonly}
191
+ />
192
+ </div>
193
+ </div>
194
+ ),
195
+ id: "advanced",
196
+ });
197
+
198
+ return (
199
+ <div
200
+ className="flex h-full flex-col bg-gray-50"
201
+ data-testid="editor-sidepanel"
202
+ >
203
+ <h1 className="flex h-12 items-center justify-center border-b border-gray-200 p-2">
204
+ {editContext.selection.length > 0 && (
205
+ <SimpleIconButton
206
+ icon="pi pi-angle-left"
207
+ onClick={() => editContext.select([])}
208
+ label="Back to page"
209
+ size="large"
210
+ />
211
+ )}
212
+ {editContext.selection.length === 0 && <div className="w-12" />}
213
+ <div
214
+ className="flex-1 text-center"
215
+ id="item-name"
216
+ data-testid="component-name"
217
+ >
218
+ {item?.name}
219
+ {isShared && (
220
+ <span className="ml-2 rounded-md bg-orange-400 p-2 text-sm text-black">
221
+ Shared
222
+ </span>
223
+ )}
224
+ </div>
225
+ <SimpleIconButton
226
+ id="insert-component-button"
227
+ icon={insertMode ? "pi pi-pencil" : "pi pi-plus"}
228
+ onClick={() => toggleInsertMode()}
229
+ label={insertMode ? "Continue editing" : "Insert component"}
230
+ size="large"
231
+ disabled={!editContext.page?.item.canWriteItem || !item?.canWriteItem}
232
+ />
233
+ </h1>
234
+
235
+ {insertMode && <Insert />}
236
+ {!insertMode && (
237
+ <SimpleTabs
238
+ key="editor-tabs"
239
+ tabs={tabPanels}
240
+ className="flex flex-1 items-center justify-center border-b border-gray-200 bg-gray-50 py-3 text-sm"
241
+ activeTab={tabPanels.findIndex((x) => x.id === activeTabKey) || 0}
242
+ setActiveTab={(index) => setActiveTabKey(tabPanels[index]?.id!)}
243
+ ></SimpleTabs>
244
+ )}
245
+ </div>
246
+ );
247
+ }
@@ -0,0 +1,351 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { useEditContext } from "../client/editContext";
3
+ import { PlaceholderDropZones } from "../page-editor-chrome/PlaceholderDropZones";
4
+ import { FieldEditedIndicators } from "../page-editor-chrome/FieldEditedIndicators";
5
+ import { PageViewContext } from "./pageViewContext";
6
+ import { CommentHighlightings } from "../page-editor-chrome/CommentHighlightings";
7
+
8
+ export function MiniMap({
9
+ deviceHeight,
10
+ scroll,
11
+ mainViewIframeRef,
12
+ mode,
13
+ pageViewContext,
14
+ }: {
15
+ deviceHeight?: number;
16
+ scroll: number;
17
+ mainViewIframeRef: React.RefObject<HTMLIFrameElement | null>;
18
+ mode: "edit" | "compare" | "view";
19
+ pageViewContext: PageViewContext;
20
+ }) {
21
+ const editContext = useEditContext();
22
+
23
+ const [scale, setScale] = React.useState(1);
24
+ const minimapRef = useRef<HTMLDivElement>(null);
25
+ const scaleContainerRef = useRef<HTMLDivElement>(null);
26
+
27
+ const iframeRef = useRef<HTMLIFrameElement>(null);
28
+ const [minimapReady, setMinimapReady] = React.useState(false);
29
+ const [refreshScale, setRefreshScale] = React.useState(false);
30
+
31
+ const observerRef = useRef<MutationObserver | null>(null);
32
+
33
+ const editorIframeRef = mainViewIframeRef;
34
+ const minimapContainerRef = useRef<HTMLDivElement>(null);
35
+
36
+ if (!mainViewIframeRef.current) return;
37
+
38
+ const scrollContainer =
39
+ editorIframeRef.current?.contentWindow?.document.scrollingElement ||
40
+ editorIframeRef.current?.contentWindow?.document.body;
41
+
42
+ const calcAndUpdateScale = () => {
43
+ const viewport = pageViewContext.viewport;
44
+ const minimapHeight = minimapContainerRef.current?.clientHeight;
45
+ if (!viewport || !scrollContainer) return;
46
+ const width = viewport.width;
47
+ const scaleX = (editContext?.configuration.outline.width || 100) / width;
48
+ const scaleY =
49
+ Math.max(deviceHeight || 0, minimapHeight || viewport.height) /
50
+ scrollContainer.scrollHeight!;
51
+
52
+ const newScale = Math.min(scaleX, scaleY, 1);
53
+ updateScale(newScale);
54
+ setScale(newScale);
55
+ return newScale;
56
+ };
57
+ // Prevent scale/width feedback loop by tracking previous values
58
+ const prevScaleRef = useRef(scale);
59
+ const prevWidthRef = useRef(0);
60
+
61
+ const miniMapWidth = useMemo(() => {
62
+ const currentWidth = Math.max(
63
+ 30,
64
+ scale * (pageViewContext.viewport.width || 0)
65
+ );
66
+
67
+ // Only update if change is significant (>1px) to avoid minor oscillations
68
+ if (
69
+ prevWidthRef.current < currentWidth ||
70
+ (Math.abs(currentWidth - prevWidthRef.current) > 1 &&
71
+ Math.abs(scale - prevScaleRef.current) > 0.01)
72
+ ) {
73
+ prevWidthRef.current = currentWidth;
74
+ prevScaleRef.current = scale;
75
+ return currentWidth;
76
+ }
77
+
78
+ return prevWidthRef.current;
79
+ }, [scale, scrollContainer?.clientWidth, pageViewContext.viewport]);
80
+
81
+ function updateCorrespondingNode(node: HTMLElement) {
82
+ const path = [];
83
+ let current: Node | null = node;
84
+
85
+ if (node?.tagName === "HTML") return;
86
+
87
+ while (
88
+ current &&
89
+ current !== current.ownerDocument?.documentElement &&
90
+ current.parentNode
91
+ ) {
92
+ const index = Array.prototype.indexOf.call(
93
+ current.parentNode.childNodes,
94
+ current
95
+ );
96
+ path.unshift(index);
97
+ current = current.parentNode;
98
+ }
99
+
100
+ const minimapDoc = iframeRef.current?.contentDocument;
101
+
102
+ if (!minimapDoc) return;
103
+ let correspondingNode: HTMLElement | null = minimapDoc.documentElement;
104
+ let originalNode: HTMLElement | null | undefined =
105
+ mainViewIframeRef.current?.contentWindow?.document.documentElement;
106
+ for (const index of path) {
107
+ if (originalNode && correspondingNode) {
108
+ if (
109
+ originalNode.childNodes.length !== correspondingNode.childNodes.length
110
+ ) {
111
+ correspondingNode.innerHTML = originalNode.innerHTML;
112
+ return;
113
+ }
114
+ }
115
+
116
+ if (originalNode) {
117
+ originalNode = originalNode.childNodes[index] as HTMLElement;
118
+ }
119
+ if (correspondingNode) {
120
+ correspondingNode = correspondingNode.childNodes[index] as HTMLElement;
121
+ }
122
+ }
123
+
124
+ if (correspondingNode && originalNode) {
125
+ if (originalNode.tagName === "HEAD")
126
+ correspondingNode.innerHTML = originalNode.innerHTML;
127
+ else {
128
+ correspondingNode.outerHTML = originalNode.outerHTML;
129
+ }
130
+ }
131
+ }
132
+
133
+ const handleLoad = () => {
134
+ if (!mainViewIframeRef?.current?.contentWindow?.document) return;
135
+
136
+ mirrorIframeContent(editorIframeRef.current!, iframeRef.current!);
137
+
138
+ const observer = new MutationObserver((mutationsList) => {
139
+ const minimapDoc = iframeRef.current?.contentDocument;
140
+ if (!minimapDoc) return;
141
+
142
+ // get distinct list of target nodes
143
+ const targetNodes = mutationsList
144
+ .filter((x) => x.type === "childList" && x.target.parentElement)
145
+ .map((m) => m.target)
146
+ .concat(
147
+ mutationsList
148
+ .filter(
149
+ (x) =>
150
+ x.type === "characterData" ||
151
+ (x.type === "attributes" && x.target.parentElement)
152
+ )
153
+ .map((m) => m.target.parentElement!)
154
+ );
155
+
156
+ const distinctTargetNodes = Array.from(new Set(targetNodes));
157
+
158
+ const nodesToRemove = new Set<Node>();
159
+
160
+ for (let i = 0; i < distinctTargetNodes.length; i++) {
161
+ for (let j = 0; j < distinctTargetNodes.length; j++) {
162
+ if (i !== j) {
163
+ if (
164
+ distinctTargetNodes[i]?.contains(distinctTargetNodes[j] as Node)
165
+ ) {
166
+ nodesToRemove.add(distinctTargetNodes[j] as Node);
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ const filteredNodes = distinctTargetNodes.filter(
173
+ (node) => !nodesToRemove.has(node)
174
+ );
175
+
176
+ filteredNodes.forEach((targetNode) => {
177
+ updateCorrespondingNode(targetNode as HTMLElement);
178
+ });
179
+
180
+ setRefreshScale((x) => !x);
181
+ });
182
+ observer.observe(mainViewIframeRef.current.contentWindow!.document!, {
183
+ childList: true, // observe direct children changes
184
+ subtree: true, // observe all descendants changes
185
+ characterData: true, // observe text changes
186
+ attributes: true, // observe attribute changes (like style or class)
187
+ });
188
+
189
+ setMinimapReady(true);
190
+ observerRef.current = observer;
191
+ };
192
+
193
+ useEffect(() => {
194
+ if (editorIframeRef?.current) {
195
+ editorIframeRef?.current.addEventListener("load", handleLoad);
196
+ }
197
+
198
+ handleLoad();
199
+
200
+ // Cleanup function
201
+ return () => {
202
+ editorIframeRef?.current?.removeEventListener("load", handleLoad);
203
+ };
204
+ }, [mainViewIframeRef.current, iframeRef.current]);
205
+
206
+ useEffect(() => {
207
+ calcAndUpdateScale();
208
+ }, [
209
+ refreshScale,
210
+ editorIframeRef,
211
+ //pageViewContext?.windowSize,
212
+ pageViewContext?.zoom,
213
+ pageViewContext.viewport,
214
+ pageViewContext.deviceHeight,
215
+ pageViewContext.deviceWidth,
216
+ ]);
217
+
218
+ if (!editContext) {
219
+ return null;
220
+ }
221
+
222
+ const scrollDocumentTo = (y: number) => {
223
+ if (!editorIframeRef?.current) return;
224
+
225
+ if (!scrollContainer) return;
226
+
227
+ scrollContainer.scrollTo({
228
+ top: y,
229
+ behavior: "smooth",
230
+ });
231
+ };
232
+
233
+ const scrollHeight = scrollContainer?.scrollHeight;
234
+ const scrollTop = scroll;
235
+
236
+ const viewport = pageViewContext.viewport;
237
+
238
+ function mirrorIframeContent(
239
+ sourceIframe: HTMLIFrameElement,
240
+ targetIframe: HTMLIFrameElement
241
+ ) {
242
+ if (sourceIframe.contentDocument && targetIframe.contentDocument) {
243
+ const sourceHtml = sourceIframe.contentDocument.documentElement.innerHTML;
244
+ targetIframe.contentDocument.documentElement.innerHTML = sourceHtml;
245
+ targetIframe.contentDocument.documentElement.style.overflow = "hidden";
246
+ }
247
+
248
+ calcAndUpdateScale();
249
+ }
250
+
251
+ const updateScale = (scale: number) => {
252
+ if (!iframeRef.current || !scaleContainerRef.current) return;
253
+
254
+ scaleContainerRef.current.style.transform = `scale(${scale})`;
255
+ scaleContainerRef.current.style.transformOrigin = "0 0";
256
+
257
+ iframeRef.current.style.width = `${scrollContainer?.clientWidth}px`;
258
+ iframeRef.current.style.height = `${scrollContainer?.scrollHeight}px`;
259
+ };
260
+
261
+ const handleClick = (e: React.MouseEvent) => {
262
+ if (!scrollContainer) return;
263
+ const iframeRect = iframeRef.current!.getBoundingClientRect();
264
+ const absY = e.clientY - iframeRect.top;
265
+ const relY = (absY / iframeRect.height) * scrollContainer.scrollHeight;
266
+
267
+ e.stopPropagation();
268
+ scrollDocumentTo(relY - viewport.height / 2);
269
+ };
270
+
271
+ const handleScroll = useCallback(
272
+ (e: React.WheelEvent) => {
273
+ if (!scrollContainer) return;
274
+ scrollContainer.scrollBy({
275
+ behavior: "instant",
276
+ left: 0,
277
+ top: e.deltaY,
278
+ });
279
+ editorIframeRef.current?.contentWindow?.document.documentElement?.scrollBy(
280
+ {
281
+ behavior: "instant",
282
+ left: 0,
283
+ top: e.deltaY,
284
+ }
285
+ );
286
+ e.stopPropagation();
287
+ },
288
+ [editorIframeRef]
289
+ );
290
+
291
+ return (
292
+ <div
293
+ className="bg-white z-50 relative overflow-hidden"
294
+ ref={minimapContainerRef}
295
+ style={{
296
+ height: "100%",
297
+ width: miniMapWidth + "px",
298
+ boxShadow: "5px 0 5px -5px #333",
299
+ }}
300
+ >
301
+ <div
302
+ className="absolute bg-opacity-50 overflow-hidden select-none inset-0"
303
+ style={{
304
+ boxShadow: "rgb(200, 200, 200) -10px 1px 13px -10px",
305
+ }}
306
+ >
307
+ <div
308
+ ref={minimapRef}
309
+ onClickCapture={handleClick}
310
+ onDragOverCapture={handleClick}
311
+ onWheelCapture={handleScroll}
312
+ className="h-full w-full cursor-pointer"
313
+ >
314
+ <div ref={scaleContainerRef}>
315
+ <iframe
316
+ className="pointer-events-none"
317
+ ref={iframeRef}
318
+ style={{ visibility: minimapReady ? "visible" : "hidden" }}
319
+ />
320
+ </div>
321
+ </div>
322
+ </div>
323
+ {scrollTop !== undefined &&
324
+ scrollHeight !== undefined &&
325
+ minimapReady && (
326
+ <div
327
+ className="absolute bg-opacity-60 hover:bg-opacity-65 bg-gray-300 w-full pointer-events-none"
328
+ style={{
329
+ top: scrollTop * scale + "px",
330
+ height: viewport.height * scale + "px",
331
+ }}
332
+ ></div>
333
+ )}
334
+ {mode === "edit" && (
335
+ <PlaceholderDropZones
336
+ scale={scale}
337
+ iframe={iframeRef.current}
338
+ size="small"
339
+ />
340
+ )}
341
+ <FieldEditedIndicators
342
+ scale={scale}
343
+ pageViewContext={pageViewContext}
344
+ scroll={scroll}
345
+ />
346
+ {editContext.showComments && (
347
+ <CommentHighlightings iframe={iframeRef.current!} scale={scale} />
348
+ )}
349
+ </div>
350
+ );
351
+ }