@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,435 @@
1
+ import React, { useState } from "react";
2
+ import { useEffect, useRef } from "react";
3
+
4
+ import { EditContextType } from "./client/editContext";
5
+
6
+ import { FieldDescriptor } from "../types";
7
+ import { Field, FullItem, ItemDescriptor } from "./pageModel";
8
+
9
+ export type Rect = {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ };
15
+
16
+ // Hook
17
+ export function useEventListenerExt(
18
+ eventName: string,
19
+ handler: any,
20
+ element: HTMLElement | Window,
21
+ capture = false
22
+ ) {
23
+ // Create a ref that stores handler
24
+ const savedHandler = useRef<any>(undefined);
25
+
26
+ // Update ref.current value if handler changes.
27
+ // This allows our effect below to always get latest handler ...
28
+ // ... without us needing to pass it in effect deps array ...
29
+ // ... and potentially cause effect to re-run every render.
30
+ useEffect(() => {
31
+ savedHandler.current = handler;
32
+ }, [handler]);
33
+
34
+ useEffect(
35
+ () => {
36
+ // Make sure element supports addEventListener
37
+ // On
38
+ const isSupported = element && element.addEventListener;
39
+ if (!isSupported) return;
40
+
41
+ // Create event listener that calls handler function stored in ref
42
+ const eventListener = (event: Event) => savedHandler.current(event);
43
+
44
+ // Add event listener
45
+ element.addEventListener(eventName, eventListener, { capture: capture });
46
+
47
+ // Remove event listener on cleanup
48
+ return () => {
49
+ element.removeEventListener(eventName, eventListener);
50
+ };
51
+ },
52
+ [eventName, element] // Re-run if eventName or element changes
53
+ );
54
+ }
55
+
56
+ export const useLocalStorage = <T>(
57
+ storageKey: string,
58
+ fallbackState: T
59
+ ): [T, React.Dispatch<React.SetStateAction<T>>] => {
60
+ const [value, setValue] = useState<T>(
61
+ JSON.parse(
62
+ typeof window !== "undefined"
63
+ ? localStorage.getItem(storageKey) ?? JSON.stringify(fallbackState)
64
+ : JSON.stringify(fallbackState)
65
+ )
66
+ );
67
+
68
+ useEffect(() => {
69
+ if (typeof window !== "undefined")
70
+ localStorage.setItem(storageKey, JSON.stringify(value));
71
+ }, [value, storageKey]);
72
+
73
+ return [value, setValue];
74
+ };
75
+
76
+ export function isFieldActionExecuting(
77
+ field: Field,
78
+ editContext: EditContextType
79
+ ) {
80
+ const fieldItem = field.descriptor.item;
81
+
82
+ if (!fieldItem) {
83
+ return false;
84
+ }
85
+
86
+ var action = editContext.activeFieldActions.find(
87
+ (x) =>
88
+ x.field.fieldId == field.id &&
89
+ x.field.item?.id == fieldItem.id &&
90
+ x.field.item?.language == fieldItem.language &&
91
+ x.field.item?.version == fieldItem.version
92
+ );
93
+
94
+ return action?.state == "running";
95
+ }
96
+
97
+ export function getItemDescriptor(
98
+ item: FullItem | ItemDescriptor
99
+ ): ItemDescriptor {
100
+ if (!item) throw new Error("Item is null");
101
+
102
+ if ("descriptor" in item) {
103
+ return item.descriptor;
104
+ }
105
+
106
+ return {
107
+ id: item!.id,
108
+ language: item!.language,
109
+ version: item!.version,
110
+ name: item!.name,
111
+ };
112
+ }
113
+
114
+ export function getAbsoluteIconUrl(url: string) {
115
+ //return configuration.services.editorService.baseUrl + url;
116
+ return url;
117
+ }
118
+
119
+ export function findParentWithAttribute(
120
+ element: HTMLElement,
121
+ attr: string
122
+ ): HTMLElement | null {
123
+ while (element && element !== document.documentElement) {
124
+ if (element.hasAttribute(attr)) {
125
+ return element;
126
+ }
127
+ element = element.parentElement as HTMLElement;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ export function parsePlaceholderKey(key: string) {
133
+ const placeholderKeyComponents = key.split("_");
134
+
135
+ const placeholderName =
136
+ placeholderKeyComponents.length === 1
137
+ ? placeholderKeyComponents[0]
138
+ : placeholderKeyComponents[1];
139
+ const parentId =
140
+ placeholderKeyComponents.length === 1
141
+ ? undefined
142
+ : placeholderKeyComponents[0];
143
+ return {
144
+ name: placeholderName,
145
+ componentId: parentId,
146
+ };
147
+ }
148
+
149
+ // export function getFieldRawValue(field: Field, editContext: EditContextType) {
150
+ // return (
151
+ // getModifiedField(field.descriptor, editContext)?.rawValue ??
152
+ // field.rawValue ??
153
+ // field.value
154
+ // );
155
+ // }
156
+
157
+ // export function getFieldValue(field: Field, editContext: EditContextType) {
158
+ // return getModifiedField(field.descriptor, editContext)?.value ?? field.value;
159
+ // }
160
+
161
+ // export function getModifiedField(
162
+ // field: FieldDescriptor,
163
+ // editContext: EditContextType
164
+ // ) {
165
+ // const modifiedField = editContext.modifiedFields.find(
166
+ // (x) =>
167
+ // x.field.fieldId === field.fieldId &&
168
+ // x.field.item.id === field.item.id &&
169
+ // x.field.item.language === field.item.language &&
170
+ // x.field.item.version === field.item.version
171
+ // );
172
+
173
+ // return modifiedField;
174
+ // }
175
+
176
+ // export function getModifiedFieldValue(
177
+ // field: FieldDescriptor,
178
+ // editContext: EditContextType
179
+ // ) {
180
+ // return getModifiedField(field, editContext)?.value;
181
+ // }
182
+
183
+ // export function setModifiedFieldValue(
184
+ // field: FieldDescriptor,
185
+ // value: string,
186
+ // editContext: EditContextType
187
+ // ) {
188
+ // const modifiedValue = editContext.modifiedFields.find(
189
+ // (x) =>
190
+ // x.field.fieldId === field.fieldId &&
191
+ // x.field.item.id === field.item.id &&
192
+ // x.field.item.language === field.item.language &&
193
+ // x.field.item.version === field.item.version
194
+ // );
195
+
196
+ // if (modifiedValue) {
197
+ // modifiedValue.value = value;
198
+ // } else {
199
+ // editContext.modifiedFields.push({
200
+ // field: field,
201
+ // value: value,
202
+ // });
203
+ // }
204
+
205
+ // editContext.setModifiedFields([...editContext.modifiedFields]);
206
+ // }
207
+
208
+ export function hasFieldLock(
209
+ field: FieldDescriptor,
210
+ editContext: EditContextType
211
+ ) {
212
+ if (!editContext.lockedField) {
213
+ return false;
214
+ }
215
+ return (
216
+ editContext.lockedField.fieldId === field.fieldId &&
217
+ editContext.lockedField.item.id === field.item.id &&
218
+ editContext.lockedField.item.language === field.item.language &&
219
+ editContext.lockedField.item.version === field.item.version
220
+ );
221
+ }
222
+
223
+ export function getSessionWithFieldLock(
224
+ field: FieldDescriptor,
225
+ editContext: EditContextType
226
+ ) {
227
+ return editContext.activeSessions.find(
228
+ (s) =>
229
+ s.fieldLock?.fieldId === field.fieldId &&
230
+ s.fieldLock?.fieldId === field.fieldId &&
231
+ s.fieldLock?.item.id === field.item.id &&
232
+ s.fieldLock?.item.language === field.item.language &&
233
+ s.fieldLock?.item.version === field.item.version
234
+ );
235
+ }
236
+
237
+ export function getFieldDescriptorFromElement(
238
+ element: Element
239
+ ): FieldDescriptor {
240
+ return {
241
+ item: {
242
+ id: element.getAttribute("data-itemid")!,
243
+ version: parseInt(element.getAttribute("data-version")!),
244
+ language: element.getAttribute("data-language")!,
245
+ },
246
+ fieldId: element.getAttribute("data-fieldid")!,
247
+ };
248
+ }
249
+
250
+ export function findNearestComponentId(startElement: Element) {
251
+ let element: Element | null = startElement;
252
+ while (element) {
253
+ if (element.hasAttribute("data-component-start")) {
254
+ return element.getAttribute("data-component-start") ?? undefined;
255
+ }
256
+ const prev: Element | null = element.previousElementSibling;
257
+ if (!prev) {
258
+ element = element.parentElement;
259
+ } else element = prev;
260
+ }
261
+ return;
262
+ }
263
+
264
+ export function findFieldElement(
265
+ iframe: HTMLIFrameElement,
266
+ fieldDescriptor: FieldDescriptor
267
+ ) {
268
+ if (!iframe.contentWindow?.document.body) return undefined;
269
+
270
+ const fieldElement = iframe.contentWindow?.document.body.querySelector(
271
+ "[data-itemid='" +
272
+ fieldDescriptor.item.id +
273
+ "'][data-language='" +
274
+ fieldDescriptor.item.language +
275
+ "'][data-version='" +
276
+ fieldDescriptor.item.version +
277
+ "'][data-fieldid='" +
278
+ fieldDescriptor.fieldId +
279
+ "']"
280
+ );
281
+
282
+ return fieldElement;
283
+ }
284
+
285
+ export function findComponentRect(
286
+ iframe: HTMLIFrameElement,
287
+ componentId: string,
288
+ takeScrollPositionIntoAccount = false
289
+ ) {
290
+ if (!iframe.contentWindow?.document.body) return undefined;
291
+
292
+ const componentStart = iframe.contentWindow?.document.body.querySelector(
293
+ "[data-component-start='" + componentId + "']"
294
+ );
295
+
296
+ const componentEnd = iframe?.contentWindow?.document.body.querySelector(
297
+ "[data-component-end='" + componentId + "']"
298
+ );
299
+
300
+ if (!componentStart || !componentEnd) {
301
+ return null;
302
+ }
303
+
304
+ let startElement = componentStart.nextElementSibling;
305
+
306
+ while (startElement && startElement.tagName === "SCRIPT") {
307
+ startElement = startElement.nextElementSibling;
308
+ }
309
+
310
+ if (!startElement) {
311
+ console.error("No start element found for component " + componentId);
312
+ return null;
313
+ }
314
+
315
+ const startRect = takeScrollPositionIntoAccount
316
+ ? getAbsolutePosition(startElement as HTMLElement, iframe)
317
+ : startElement.getBoundingClientRect();
318
+
319
+ let endElement = componentEnd.previousElementSibling;
320
+
321
+ while (endElement && endElement.tagName === "SCRIPT") {
322
+ endElement = endElement.previousElementSibling;
323
+ }
324
+
325
+ if (!endElement) return null;
326
+
327
+ const endRect = takeScrollPositionIntoAccount
328
+ ? getAbsolutePosition(endElement as HTMLElement, iframe)
329
+ : endElement.getBoundingClientRect();
330
+
331
+ const elements =
332
+ startElement === endElement ? [startElement] : [startElement, endElement];
333
+
334
+ // console.log(
335
+ // "start rect",
336
+ // startRect,
337
+ // startElement,
338
+ // iframe.contentWindow?.scrollY,
339
+ // takeScrollPositionIntoAccount
340
+ // );
341
+
342
+ return {
343
+ rect: {
344
+ x: startRect.x,
345
+ y: startRect.y,
346
+ width: endRect.x + endRect.width - startRect.x,
347
+ height: endRect.y + endRect.height - startRect.y,
348
+ },
349
+ elements: elements,
350
+ };
351
+ }
352
+
353
+ export const getAbsolutePosition = (
354
+ element: HTMLElement,
355
+ iframe: HTMLIFrameElement
356
+ ) => {
357
+ const rect = element.getBoundingClientRect();
358
+ const scrollLeft =
359
+ iframe.contentWindow?.scrollX ||
360
+ iframe.contentDocument?.documentElement.scrollLeft || 0;
361
+ const scrollTop =
362
+ iframe.contentWindow?.scrollY ||
363
+ iframe.contentDocument?.documentElement.scrollTop || 0;
364
+
365
+
366
+
367
+ return {
368
+ x: rect.left + scrollLeft,
369
+ y: rect.top + scrollTop,
370
+ width: rect.width,
371
+ height: rect.height,
372
+ };
373
+ };
374
+
375
+ export function findParentComponentId(element: HTMLElement) {
376
+ while (element && element !== document.documentElement) {
377
+ const startElement = findStartElement(element);
378
+
379
+ if (startElement) {
380
+ return startElement.getAttribute("data-component-start");
381
+ }
382
+ element = element.parentElement as HTMLElement;
383
+ }
384
+ return null;
385
+ }
386
+
387
+ function findStartElement(element: HTMLElement) {
388
+ let prev: Element | null = element;
389
+ while (prev) {
390
+ prev = prev.previousElementSibling;
391
+ if (prev?.getAttribute("data-component-start")) {
392
+ return prev;
393
+ }
394
+ }
395
+ return null;
396
+ }
397
+
398
+ export function normalizeGuid(id: string) {
399
+ id = id.toUpperCase();
400
+
401
+ if (id.length && id[0] === "{") return id;
402
+ return "{" + id + "}";
403
+ }
404
+
405
+ const dateOptions: Intl.DateTimeFormatOptions = {
406
+ year: "numeric",
407
+ month: "numeric",
408
+ day: "numeric",
409
+ hour: "numeric",
410
+ minute: "numeric",
411
+ second: "numeric",
412
+ hour12: false,
413
+ };
414
+
415
+ const dateFormat = Intl.DateTimeFormat(typeof navigator !== 'undefined' ? navigator.language : 'en', dateOptions);
416
+
417
+ export function formatDate(date: Date) {
418
+ return dateFormat.format(date);
419
+ }
420
+
421
+ export function findClosestFieldElement(node: Node | null): HTMLElement | null {
422
+ let current: Node | null = node;
423
+
424
+ while (current) {
425
+ if (current.nodeType === Node.ELEMENT_NODE) {
426
+ const element = current as Element;
427
+ if (element.hasAttribute("data-fieldid")) {
428
+ return element as HTMLElement;
429
+ }
430
+ }
431
+ current = current.parentNode;
432
+ }
433
+
434
+ return null;
435
+ }
@@ -0,0 +1,256 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useEditContext } from "../client/editContext";
3
+ import { LanguageSelector } from "../menubar/LanguageSelector";
4
+ import { VersionSelector } from "../menubar/VersionSelector";
5
+ import { FullItem, ItemDescriptor, Version } from "../pageModel";
6
+ import { usePageViewContext } from "../page-viewer/pageViewContext";
7
+ import { useSearchParams } from "next/navigation";
8
+ import { useRouter } from "next/navigation";
9
+ import { SingleEditView } from "./SingleEditView";
10
+ import { getLanguagesAndVersions } from "../services/contentService";
11
+ import { Allotment } from "allotment";
12
+ import { ItemEditor } from "./ItemEditor";
13
+
14
+ export function CompareView() {
15
+ const editContext = useEditContext();
16
+ const searchParams = useSearchParams();
17
+ const router = useRouter();
18
+ if (!editContext) return;
19
+
20
+ const [compareToItem, setCompareToItem] = useState<FullItem | undefined>();
21
+ const [compareToVersions, setCompareToVersions] = useState<Version[]>([]);
22
+
23
+ const compareTo = editContext.compareTo;
24
+ const setCompareTo = editContext.setCompareTo;
25
+
26
+ const comparePageViewContext = usePageViewContext({
27
+ pageItemDescriptor: compareToItem?.descriptor,
28
+ itemsRepository: editContext.itemsRepository,
29
+ configuration: editContext.configuration,
30
+ });
31
+
32
+ useEffect(() => {
33
+ comparePageViewContext.setDevice(editContext.pageView.device);
34
+ }, [editContext.pageView.device]);
35
+
36
+ const updateCompareToVersions = async () => {
37
+ if (!compareTo || !compareTo.id || !compareTo.language) {
38
+ setCompareToVersions([]);
39
+ return;
40
+ }
41
+ const result = await getLanguagesAndVersions(compareTo);
42
+ if (result?.data) {
43
+ const versions = result.data.versions.reverse();
44
+ setCompareToVersions(versions);
45
+ }
46
+ };
47
+
48
+ useEffect(() => {
49
+ const updateCompareTo = async () => {
50
+ if (!editContext) return;
51
+ if (editContext.contentEditorItem) {
52
+ let urlLanguage =
53
+ searchParams.get("compareLanguage") ||
54
+ editContext.contentEditorItem.descriptor.language;
55
+ let urlVersion =
56
+ parseInt(searchParams.get("compareVersion") || "0") ||
57
+ editContext.currentItemDescriptor?.version;
58
+
59
+ const language = editContext.itemLanguages.find(
60
+ (x) => x.languageCode === urlLanguage
61
+ );
62
+
63
+ if (!language || !language.versions) {
64
+ urlLanguage = "";
65
+ }
66
+
67
+ if (!editContext.currentItemDescriptor?.id || !language) {
68
+ setCompareTo(undefined);
69
+ return;
70
+ }
71
+
72
+ const result = await getLanguagesAndVersions({
73
+ id: editContext.currentItemDescriptor?.id,
74
+ language: language.languageCode,
75
+ version: 0,
76
+ });
77
+ if (result?.data) {
78
+ const versions = [...result.data.versions].reverse();
79
+ const version = versions.find((x) => x.version === urlVersion);
80
+
81
+ setCompareTo({
82
+ id: editContext.contentEditorItem.descriptor.id,
83
+ language: urlLanguage,
84
+ version: version?.version || 0,
85
+ });
86
+ }
87
+ }
88
+ };
89
+ updateCompareTo();
90
+ }, [searchParams, editContext?.itemLanguages]);
91
+
92
+ useEffect(() => {
93
+ const updateCompareToItem = async () => {
94
+ if (!compareTo || !compareTo.id || !compareTo.language) {
95
+ setCompareToItem(undefined);
96
+ return;
97
+ }
98
+ const item = await editContext?.itemsRepository.getItem(compareTo);
99
+ setCompareToItem(item);
100
+ };
101
+ updateCompareToItem();
102
+ updateCompareToVersions();
103
+ }, [compareTo]);
104
+
105
+ function updateCompareTo(compareToItemDescriptor: ItemDescriptor) {
106
+ if (compareToItemDescriptor) {
107
+ const newParams = new URLSearchParams(searchParams.toString());
108
+ newParams.set("compareLanguage", compareToItemDescriptor.language);
109
+ newParams.set(
110
+ "compareVersion",
111
+ compareToItemDescriptor.version.toString()
112
+ );
113
+ router.push(`?${newParams.toString()}`);
114
+ }
115
+ }
116
+
117
+ const originalSelector = (
118
+ <div className="flex-1 bg-gray-100 border-b flex items-center justify-center flex-shrink-0 text-sm text-gray-600 p-1">
119
+ <LanguageSelector
120
+ selectedLanguage={editContext.contentEditorItem?.descriptor?.language}
121
+ showAllLanguagesSwitch={false}
122
+ darkMode
123
+ onLanguageSelected={(language) => {
124
+ if (!editContext.contentEditorItem) return;
125
+ editContext.loadItem({
126
+ id: editContext.contentEditorItem.descriptor.id,
127
+ language: language.languageCode,
128
+ version: 0,
129
+ });
130
+ }}
131
+ />
132
+ {compareTo?.language && (
133
+ <VersionSelector
134
+ darkMode
135
+ readOnly={true}
136
+ versions={editContext.itemVersions}
137
+ itemDescriptor={editContext.currentItemDescriptor}
138
+ actualVersion={editContext.contentEditorItem?.version}
139
+ onVersionSelected={(version) => {
140
+ if (!editContext.contentEditorItem) return;
141
+ editContext.loadItem({
142
+ id: editContext.contentEditorItem.descriptor.id,
143
+ language: editContext.contentEditorItem.descriptor.language,
144
+ version,
145
+ });
146
+ }}
147
+ />
148
+ )}
149
+ </div>
150
+ );
151
+
152
+ const compareToSelector = (
153
+ <div className="flex-1 bg-gray-100 border-b flex items-center justify-center flex-shrink-0 text-sm text-gray-600 p-1">
154
+ <LanguageSelector
155
+ selectedLanguage={compareTo?.language}
156
+ showAllLanguagesSwitch={false}
157
+ darkMode
158
+ onLanguageSelected={(language) => {
159
+ if (!compareTo?.id) return;
160
+ updateCompareTo({
161
+ id: compareTo.id,
162
+ language: language.languageCode,
163
+ version: compareTo.version,
164
+ });
165
+ }}
166
+ />
167
+ {compareTo?.language && (
168
+ <VersionSelector
169
+ darkMode
170
+ readOnly={true}
171
+ versions={compareToVersions}
172
+ itemDescriptor={compareTo}
173
+ actualVersion={compareToItem?.version}
174
+ onVersionSelected={(version) => {
175
+ if (!compareTo?.id) return;
176
+ updateCompareTo({
177
+ id: compareTo.id,
178
+ language: compareTo.language,
179
+ version,
180
+ });
181
+ }}
182
+ />
183
+ )}
184
+ </div>
185
+ );
186
+
187
+ if (
188
+ (compareToItem && !compareToItem.hasLayout) ||
189
+ comparePageViewContext.device === ""
190
+ )
191
+ return (
192
+ <div className="flex flex-col h-full w-full items-stretch">
193
+ <div className="flex w-full">
194
+ {originalSelector}
195
+ {compareToSelector}
196
+ </div>
197
+ <div className="flex-1 relative">
198
+ <div className="absolute w-full h-full">
199
+ <ItemEditor
200
+ pageViewContext={editContext.pageView}
201
+ compareToItem={compareToItem}
202
+ />
203
+ </div>
204
+ </div>
205
+ </div>
206
+ );
207
+
208
+ return (
209
+ <Allotment className="flex h-full w-full">
210
+ <Allotment.Pane>
211
+ <div className="flex flex-col h-full w-full items-stretch">
212
+ {originalSelector}
213
+ <SingleEditView
214
+ key="original"
215
+ name="original"
216
+ mode={editContext?.previewMode ? "view" : "edit"}
217
+ pageViewContext={editContext.pageView}
218
+ itemDescriptor={editContext.contentEditorItem?.descriptor}
219
+ />
220
+ </div>
221
+ </Allotment.Pane>
222
+ <Allotment.Pane>
223
+ <div className="flex flex-col h-full w-full items-stretch">
224
+ {compareToSelector}
225
+ {compareTo?.language && (
226
+ <SingleEditView
227
+ key="compareTo"
228
+ name="compareTo"
229
+ mode={
230
+ compareToItem?.id ===
231
+ editContext.contentEditorItem?.descriptor.id &&
232
+ compareToItem?.language ===
233
+ editContext.contentEditorItem?.descriptor.language &&
234
+ compareToItem?.version ===
235
+ editContext.contentEditorItem?.descriptor.version
236
+ ? editContext?.previewMode
237
+ ? "view"
238
+ : "edit"
239
+ : "compare"
240
+ }
241
+ pageViewContext={comparePageViewContext}
242
+ itemDescriptor={compareTo}
243
+ />
244
+ )}
245
+ {!compareTo?.language && (
246
+ <div className="flex flex-col h-full w-full items-center justify-center">
247
+ <div className="text-gray-400">
248
+ Select a language and version to compare
249
+ </div>
250
+ </div>
251
+ )}
252
+ </div>
253
+ </Allotment.Pane>
254
+ </Allotment>
255
+ );
256
+ }
@@ -0,0 +1,27 @@
1
+ import { useEditContext } from "../client/editContext";
2
+ import { CompareView } from "./CompareView";
3
+ import { SingleEditView } from "./SingleEditView";
4
+
5
+ export function EditView() {
6
+ const editContext = useEditContext();
7
+ if (!editContext) return null;
8
+
9
+ if (!editContext.contentEditorItem?.descriptor)
10
+ return (
11
+ <div className="grid items-center justify-center h-full w-full">
12
+ <div className="text-sm text-gray-500">Not item selected</div>
13
+ </div>
14
+ );
15
+
16
+ if (editContext.compareMode) return <CompareView />;
17
+
18
+ return (
19
+ <SingleEditView
20
+ key="single"
21
+ name="single"
22
+ mode={editContext?.previewMode ? "view" : "edit"}
23
+ pageViewContext={editContext.pageView}
24
+ itemDescriptor={editContext.contentEditorItem?.descriptor}
25
+ />
26
+ );
27
+ }