@alpaca-editor/core 1.0.3815 → 1.0.3817

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 (119) hide show
  1. package/dist/config/config.js +1 -1
  2. package/dist/config/config.js.map +1 -1
  3. package/dist/editor/EditorWarnings.js +1 -1
  4. package/dist/editor/EditorWarnings.js.map +1 -1
  5. package/dist/editor/FieldList.js +1 -1
  6. package/dist/editor/FieldList.js.map +1 -1
  7. package/dist/editor/FieldListFieldWithFallbacks.js +3 -3
  8. package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
  9. package/dist/editor/Titlebar.js +1 -1
  10. package/dist/editor/Titlebar.js.map +1 -1
  11. package/dist/editor/client/EditorClient.js +71 -31
  12. package/dist/editor/client/EditorClient.js.map +1 -1
  13. package/dist/editor/client/editContext.d.ts +9 -3
  14. package/dist/editor/client/editContext.js.map +1 -1
  15. package/dist/editor/client/operations.d.ts +5 -1
  16. package/dist/editor/client/operations.js +97 -3
  17. package/dist/editor/client/operations.js.map +1 -1
  18. package/dist/editor/component-designer/ComponentDesigner.js +1 -1
  19. package/dist/editor/component-designer/ComponentDesigner.js.map +1 -1
  20. package/dist/editor/menubar/LanguageSelector.js +3 -3
  21. package/dist/editor/menubar/LanguageSelector.js.map +1 -1
  22. package/dist/editor/menubar/PageSelector.js +1 -1
  23. package/dist/editor/menubar/PageSelector.js.map +1 -1
  24. package/dist/editor/menubar/PageViewerControls.js +12 -7
  25. package/dist/editor/menubar/PageViewerControls.js.map +1 -1
  26. package/dist/editor/menubar/Separator.js +1 -1
  27. package/dist/editor/menubar/VersionSelector.js +1 -1
  28. package/dist/editor/menubar/VersionSelector.js.map +1 -1
  29. package/dist/editor/page-editor-chrome/FrameMenu.d.ts +2 -2
  30. package/dist/editor/page-editor-chrome/FrameMenu.js +21 -15
  31. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  32. package/dist/editor/page-editor-chrome/FrameMenus.d.ts +2 -2
  33. package/dist/editor/page-editor-chrome/FrameMenus.js +2 -2
  34. package/dist/editor/page-editor-chrome/FrameMenus.js.map +1 -1
  35. package/dist/editor/page-editor-chrome/InlineEditor.d.ts +2 -2
  36. package/dist/editor/page-editor-chrome/InlineEditor.js +175 -17
  37. package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
  38. package/dist/editor/page-editor-chrome/PageEditorChrome.d.ts +2 -2
  39. package/dist/editor/page-editor-chrome/PageEditorChrome.js +2 -2
  40. package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
  41. package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
  42. package/dist/editor/page-viewer/EditorForm.js +9 -8
  43. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  44. package/dist/editor/page-viewer/MiniMap.d.ts +2 -2
  45. package/dist/editor/page-viewer/MiniMap.js +2 -2
  46. package/dist/editor/page-viewer/MiniMap.js.map +1 -1
  47. package/dist/editor/page-viewer/PageViewer.d.ts +2 -2
  48. package/dist/editor/page-viewer/PageViewer.js +3 -3
  49. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  50. package/dist/editor/page-viewer/PageViewerFrame.d.ts +2 -2
  51. package/dist/editor/page-viewer/PageViewerFrame.js +12 -12
  52. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  53. package/dist/editor/reviews/Comments.d.ts +2 -0
  54. package/dist/editor/reviews/Comments.js +26 -9
  55. package/dist/editor/reviews/Comments.js.map +1 -1
  56. package/dist/editor/reviews/DiffView.d.ts +17 -0
  57. package/dist/editor/reviews/DiffView.js +57 -0
  58. package/dist/editor/reviews/DiffView.js.map +1 -0
  59. package/dist/editor/reviews/SuggestedEdit.d.ts +4 -0
  60. package/dist/editor/reviews/SuggestedEdit.js +180 -0
  61. package/dist/editor/reviews/SuggestedEdit.js.map +1 -0
  62. package/dist/editor/services/suggestedEditsService.d.ts +17 -0
  63. package/dist/editor/services/suggestedEditsService.js +26 -0
  64. package/dist/editor/services/suggestedEditsService.js.map +1 -0
  65. package/dist/editor/ui/PerfectTree.js +3 -3
  66. package/dist/editor/ui/PerfectTree.js.map +1 -1
  67. package/dist/editor/ui/SimpleIconButton.js +3 -1
  68. package/dist/editor/ui/SimpleIconButton.js.map +1 -1
  69. package/dist/editor/views/CompareView.js +4 -13
  70. package/dist/editor/views/CompareView.js.map +1 -1
  71. package/dist/editor/views/EditView.js +2 -2
  72. package/dist/editor/views/EditView.js.map +1 -1
  73. package/dist/editor/views/SingleEditView.d.ts +2 -2
  74. package/dist/editor/views/SingleEditView.js +2 -2
  75. package/dist/editor/views/SingleEditView.js.map +1 -1
  76. package/dist/lib/safelist.js +1 -1
  77. package/dist/lib/safelist.js.map +1 -1
  78. package/dist/page-wizard/steps/BuildPageStep.js +2 -2
  79. package/dist/page-wizard/steps/BuildPageStep.js.map +1 -1
  80. package/dist/page-wizard/steps/CreatePageAndLayoutStep.js +2 -2
  81. package/dist/page-wizard/steps/CreatePageAndLayoutStep.js.map +1 -1
  82. package/dist/styles.css +36 -2
  83. package/dist/types.d.ts +18 -0
  84. package/package.json +4 -1
  85. package/src/config/config.tsx +2 -2
  86. package/src/editor/EditorWarnings.tsx +2 -2
  87. package/src/editor/FieldList.tsx +6 -6
  88. package/src/editor/FieldListFieldWithFallbacks.tsx +9 -9
  89. package/src/editor/Titlebar.tsx +4 -4
  90. package/src/editor/client/EditorClient.tsx +83 -51
  91. package/src/editor/client/editContext.ts +12 -3
  92. package/src/editor/client/operations.ts +146 -9
  93. package/src/editor/component-designer/ComponentDesigner.tsx +1 -1
  94. package/src/editor/menubar/LanguageSelector.tsx +6 -6
  95. package/src/editor/menubar/PageSelector.tsx +11 -11
  96. package/src/editor/menubar/PageViewerControls.tsx +49 -23
  97. package/src/editor/menubar/Separator.tsx +2 -2
  98. package/src/editor/menubar/VersionSelector.tsx +1 -1
  99. package/src/editor/page-editor-chrome/FrameMenu.tsx +18 -17
  100. package/src/editor/page-editor-chrome/FrameMenus.tsx +6 -6
  101. package/src/editor/page-editor-chrome/InlineEditor.tsx +233 -22
  102. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +11 -14
  103. package/src/editor/page-viewer/EditorForm.tsx +15 -9
  104. package/src/editor/page-viewer/MiniMap.tsx +4 -4
  105. package/src/editor/page-viewer/PageViewer.tsx +6 -6
  106. package/src/editor/page-viewer/PageViewerFrame.tsx +19 -13
  107. package/src/editor/reviews/Comments.tsx +56 -15
  108. package/src/editor/reviews/DiffView.tsx +109 -0
  109. package/src/editor/reviews/SuggestedEdit.tsx +316 -0
  110. package/src/editor/services/suggestedEditsService.ts +39 -0
  111. package/src/editor/ui/PerfectTree.tsx +5 -5
  112. package/src/editor/ui/SimpleIconButton.tsx +5 -3
  113. package/src/editor/views/CompareView.tsx +13 -24
  114. package/src/editor/views/EditView.tsx +2 -2
  115. package/src/editor/views/SingleEditView.tsx +3 -3
  116. package/src/lib/safelist.tsx +2 -0
  117. package/src/page-wizard/steps/BuildPageStep.tsx +18 -25
  118. package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +16 -18
  119. package/src/types.ts +19 -0
@@ -49,7 +49,7 @@ export function PageSelector({
49
49
  <>
50
50
  <div
51
51
  id="page-selector-button"
52
- className="p-[7px] text-sm cursor-pointer flex items-center gap-3 text-gray-200 hover:bg-gray-500 rounded-md"
52
+ className="flex cursor-pointer items-center gap-3 rounded-md p-[7px] py-[5px] text-sm text-gray-200 hover:bg-gray-500"
53
53
  onClick={(ev) => overlaypanel.current?.toggle(ev, ev.currentTarget)}
54
54
  data-testid="page-selector-button"
55
55
  >
@@ -66,10 +66,10 @@ export function PageSelector({
66
66
  </div>
67
67
  </div>
68
68
  <OverlayPanel dismissable={true} ref={overlaypanel} closeOnEscape>
69
- <div className="h-[75vh] min-w-48 flex flex-col overflow-hidden">
70
- <div className="flex-1 flex flex-col gap-1">
71
- <div className="p-2 flex flex-col">
72
- <div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
69
+ <div className="flex h-[75vh] min-w-48 flex-col overflow-hidden">
70
+ <div className="flex flex-1 flex-col gap-1">
71
+ <div className="flex flex-col p-2">
72
+ <div className="mb-2 flex items-center gap-1 text-xs text-gray-500">
73
73
  <ArrowDownIcon /> Search
74
74
  </div>
75
75
  <ItemSearch
@@ -78,8 +78,8 @@ export function PageSelector({
78
78
  itemSelected={(item) => loadItem(item)}
79
79
  />
80
80
  </div>
81
- <div className="flex-1 flex flex-col">
82
- <div className="border-t p-2 pb-1 text-xs text-gray-500 flex items-center gap-1">
81
+ <div className="flex flex-1 flex-col">
82
+ <div className="flex items-center gap-1 border-t p-2 pb-1 text-xs text-gray-500">
83
83
  <ArrowDownIcon /> Select
84
84
  </div>
85
85
  <div className="relative flex-1">
@@ -91,8 +91,8 @@ export function PageSelector({
91
91
  </div>
92
92
  </div>
93
93
  </div>
94
- <div className="border-t p-2 flex flex-col">
95
- <div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
94
+ <div className="flex flex-col border-t p-2">
95
+ <div className="mb-2 flex items-center gap-1 text-xs text-gray-500">
96
96
  <ArrowDownIcon /> Last visited
97
97
  </div>
98
98
  <BrowseHistory
@@ -101,8 +101,8 @@ export function PageSelector({
101
101
  />
102
102
  </div>
103
103
  </div>
104
- <div className="border-t p-2 flex flex-col">
105
- <div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
104
+ <div className="flex flex-col border-t p-2">
105
+ <div className="mb-2 flex items-center gap-1 text-xs text-gray-500">
106
106
  <ArrowDownIcon /> Actions
107
107
  </div>
108
108
  <div className="flex gap-2">
@@ -3,6 +3,7 @@ import { useEditContext } from "../client/editContext";
3
3
  import { Separator } from "./Separator";
4
4
  import { CompareIcon, FormEditIcon } from "../ui/Icons";
5
5
  import { SimpleIconButton } from "../ui/SimpleIconButton";
6
+ import { Route, SquarePen, UserRoundPen, EyeIcon, Pencil } from "lucide-react";
6
7
 
7
8
  export function PageViewerControls() {
8
9
  const editContext = useEditContext();
@@ -17,16 +18,54 @@ export function PageViewerControls() {
17
18
  const setDevice = pageViewContext.setDevice;
18
19
 
19
20
  return (
20
- <div className="flex gap-2 items-center">
21
+ <div className="flex items-center gap-2">
21
22
  {hasLayout && (
22
23
  <>
23
- {" "}
24
+ {!editContext.user?.isLimitedPreviewUser && (
25
+ <SimpleIconButton
26
+ icon={<Pencil className="h-6 w-6 p-1" />}
27
+ label="Edit"
28
+ size="large"
29
+ className={classNames(
30
+ editContext.mode === "edit"
31
+ ? "bg-gray-200"
32
+ : "hover:bg-gray-200 hover:text-gray-800",
33
+ )}
34
+ onClick={() => editContext.setMode("edit")}
35
+ />
36
+ )}
37
+
38
+ <SimpleIconButton
39
+ icon={<EyeIcon className="h-6 w-6 p-1" />}
40
+ label="Preview"
41
+ size="large"
42
+ className={classNames(
43
+ editContext.mode === "preview"
44
+ ? "bg-gray-200"
45
+ : "hover:bg-gray-200 hover:text-gray-800",
46
+ )}
47
+ onClick={() => editContext.setMode("preview")}
48
+ />
49
+
50
+ <SimpleIconButton
51
+ selected={editContext?.mode === "suggestions"}
52
+ icon={<UserRoundPen size={24} className="p-0.5" />}
53
+ label="Suggestions"
54
+ size="large"
55
+ className={classNames(
56
+ editContext?.mode === "suggestions"
57
+ ? "text-gray-600"
58
+ : "hover:text-gray-600",
59
+ )}
60
+ onClick={() => editContext?.setMode("suggestions")}
61
+ />
62
+ <Separator size="large" />
24
63
  <i
25
64
  className={classNames(
26
65
  device === "desktop"
27
66
  ? "bg-gray-200"
28
- : " hover:bg-gray-200 text-gray-100 hover:text-gray-800",
29
- "pi pi-desktop cursor-pointer p-2 rounded-full"
67
+ : "text-gray-400 hover:bg-gray-200 hover:text-gray-800",
68
+ "pi pi-desktop cursor-pointer rounded-full p-2",
30
69
  )}
31
70
  title="Desktop"
32
71
  onClick={() => {
@@ -37,8 +76,8 @@ export function PageViewerControls() {
37
76
  className={classNames(
38
77
  device && device !== "desktop"
39
78
  ? "bg-gray-200"
40
- : " hover:bg-gray-200 text-gray-100 hover:text-gray-800",
41
- "pi pi-mobile cursor-pointer p-2 rounded-full"
79
+ : "text-gray-400 hover:bg-gray-200 hover:text-gray-800",
80
+ "pi pi-mobile cursor-pointer rounded-full p-2",
42
81
  )}
43
82
  title="Mobile"
44
83
  onClick={() => {
@@ -51,8 +90,8 @@ export function PageViewerControls() {
51
90
  className={classNames(
52
91
  !device
53
92
  ? "bg-gray-200"
54
- : " hover:bg-gray-200 text-gray-100 hover:text-gray-800",
55
- "w-8 h-8 cursor-pointer p-1 rounded-full"
93
+ : "text-gray-400 hover:bg-gray-200 hover:text-gray-800",
94
+ "h-8 w-8 cursor-pointer rounded-full p-1",
56
95
  )}
57
96
  title="Form"
58
97
  onClick={() => {
@@ -63,27 +102,14 @@ export function PageViewerControls() {
63
102
  </i>
64
103
  <Separator size="large" />
65
104
  <i
66
- className="pi pi-external-link cursor-pointer hover:bg-gray-200 p-2 rounded-full text-gray-100 hover:text-gray-800"
105
+ className="pi pi-external-link cursor-pointer rounded-full p-2 text-gray-400 hover:bg-gray-200 hover:text-gray-800"
67
106
  title="Fullscreen"
68
107
  onClick={() => pageViewContext.setFullscreen(true)}
69
108
  />
70
- {!editContext.user?.isLimitedPreviewUser && (
71
- <i
72
- className={classNames(
73
- editContext.previewMode
74
- ? "bg-gray-200"
75
- : " hover:bg-gray-200 text-gray-100 hover:text-gray-800",
76
- "pi pi-eye cursor-pointer p-2 rounded-full"
77
- )}
78
- title="Preview"
79
- onClick={() => editContext.setPreviewMode((x) => !x)}
80
- />
81
- )}
82
- <Separator size="large" />
83
109
  </>
84
110
  )}
85
111
  <SimpleIconButton
86
- icon={<CompareIcon className="w-6 h-6 p-1" />}
112
+ icon={<CompareIcon className="h-6 w-6 p-1" />}
87
113
  label="Compare"
88
114
  size="large"
89
115
  className={
@@ -4,8 +4,8 @@ export function Separator({ size }: { size?: "large" | "small" }) {
4
4
  return (
5
5
  <div
6
6
  className={classNames(
7
- "border-r border-gray-300",
8
- size === "large" ? "h-7 mx-2" : "h-4 mx-1"
7
+ "border-r border-gray-400",
8
+ size === "large" ? "mx-3 h-7" : "mx-1 h-4",
9
9
  )}
10
10
  ></div>
11
11
  );
@@ -40,7 +40,7 @@ export function VersionSelector({
40
40
  <>
41
41
  <div
42
42
  data-testid="version-selector"
43
- className={`flex cursor-pointer items-center gap-3 p-[7px] text-sm ${
43
+ className={`flex cursor-pointer items-center gap-3 p-[7px] py-[5px] text-sm ${
44
44
  darkMode
45
45
  ? "text-gray-500 hover:bg-gray-200"
46
46
  : "text-gray-200 hover:bg-gray-500"
@@ -1,19 +1,20 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
 
3
- import { EditButton, useEditContext } from "../client/editContext";
3
+ import { EditButton, useEditContext, EditorMode } from "../client/editContext";
4
4
  import { Rect, findComponentRect } from "../utils";
5
5
  import { useThrottledCallback } from "use-debounce";
6
6
  import { Component } from "../pageModel";
7
7
  import { PageViewContext } from "../page-viewer/pageViewContext";
8
8
  import { ArrowUpFromDot } from "lucide-react";
9
9
  import { cn } from "../../lib/utils";
10
+ import { CompareIcon } from "../ui/Icons";
10
11
  export function FrameMenu({
11
12
  component,
12
- mode,
13
+ compareView,
13
14
  pageViewContext,
14
15
  }: {
15
16
  component: Component;
16
- mode: "edit" | "compare" | "view";
17
+ compareView: boolean;
17
18
  pageViewContext: PageViewContext;
18
19
  }) {
19
20
  const editContext = useEditContext();
@@ -24,7 +25,6 @@ export function FrameMenu({
24
25
  const [componentRect, setComponentRect] = useState<Rect>();
25
26
  const [isHeaderWiderThanComponent, setIsHeaderWiderThanComponent] =
26
27
  useState(false);
27
- const [isHovered, setIsHovered] = useState(false);
28
28
 
29
29
  const updatePosition = () => {
30
30
  if (!component || !editContext || !pageViewContext) return;
@@ -138,7 +138,7 @@ export function FrameMenu({
138
138
  const commands = editContext.getComponentCommands([component]);
139
139
  const isDraggable =
140
140
  component.canBeMoved &&
141
- mode === "edit" &&
141
+ editContext.mode === "edit" &&
142
142
  !component.layoutId &&
143
143
  pageViewContext.page?.item.canWriteItem;
144
144
  false;
@@ -193,18 +193,18 @@ export function FrameMenu({
193
193
  }
194
194
 
195
195
  const isShared = component.isShared;
196
- const isReadonly = mode === "compare" || mode === "view";
196
+ const isReadonly = editContext.mode === "preview" || compareView;
197
197
  const isLayout = component.layoutId;
198
198
 
199
- const color = isReadonly
200
- ? "readonly"
201
- : isShared
202
- ? "shared"
203
- : isLayout
204
- ? "layout"
205
- : component.canBeMoved
206
- ? "default"
207
- : "nonMovable";
199
+ function getColor() {
200
+ if (isReadonly) return "readonly";
201
+ if (editContext?.mode === "suggestions") return "suggestions";
202
+ if (isShared) return "shared";
203
+ if (isLayout) return "layout";
204
+ if (component.canBeMoved) return "default";
205
+ return "nonMovable";
206
+ }
207
+ const color = getColor();
208
208
 
209
209
  const colorVariants = {
210
210
  shared: "border-orange-400",
@@ -212,6 +212,7 @@ export function FrameMenu({
212
212
  layout: "border-purple-400",
213
213
  default: "border-sky-400",
214
214
  nonMovable: "border-red-400",
215
+ suggestions: "border-teal-400",
215
216
  };
216
217
 
217
218
  const bgColorVariants = {
@@ -220,6 +221,7 @@ export function FrameMenu({
220
221
  layout: "bg-purple-400",
221
222
  default: "bg-sky-400",
222
223
  nonMovable: "bg-red-400",
224
+ suggestions: "bg-teal-400",
223
225
  };
224
226
 
225
227
  // Calculate initial estimation for the header width
@@ -241,7 +243,6 @@ export function FrameMenu({
241
243
  "pointer-events-none absolute inset-0 rounded-b-sm border-2",
242
244
  colorVariants[color],
243
245
  "tour-frame-menu opacity-50 hover:opacity-100",
244
- isHovered && "opacity-100",
245
246
  !isMultiSelected && isHeaderWiderThanComponent && "border-t-0",
246
247
  !isMultiSelected && !isHeaderWiderThanComponent && "rounded-tl-sm",
247
248
  isMultiSelected && "rounded-t-sm",
@@ -296,7 +297,7 @@ export function FrameMenu({
296
297
  </span>
297
298
  )}
298
299
  </div>
299
- {mode === "edit" && (
300
+ {editContext.mode === "edit" && (
300
301
  <div className="flex items-center gap-2 text-sm">
301
302
  {buttons.map((b, i) => (
302
303
  <div
@@ -1,14 +1,14 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { findComponent } from "../componentTreeHelper";
3
- import { useEditContext } from "../client/editContext";
3
+ import { useEditContext, EditorMode } from "../client/editContext";
4
4
  import { PageViewContext } from "../page-viewer/pageViewContext";
5
5
  import { FrameMenu } from "./FrameMenu";
6
6
  import { Component } from "../pageModel";
7
7
  export function FrameMenus({
8
- mode,
8
+ compareView,
9
9
  pageViewContext,
10
10
  }: {
11
- mode: "edit" | "compare" | "view";
11
+ compareView: boolean;
12
12
  pageViewContext: PageViewContext;
13
13
  }) {
14
14
  const editContext = useEditContext();
@@ -24,8 +24,8 @@ export function FrameMenus({
24
24
  .map((id) =>
25
25
  findComponent(
26
26
  id,
27
- pageViewContext.page?.rootComponent.placeholders || []
28
- )
27
+ pageViewContext.page?.rootComponent.placeholders || [],
28
+ ),
29
29
  )
30
30
  .filter((c): c is NonNullable<typeof c> => c !== undefined)
31
31
  : [];
@@ -39,7 +39,7 @@ export function FrameMenus({
39
39
  <FrameMenu
40
40
  key={c.id}
41
41
  component={c}
42
- mode={mode}
42
+ compareView={compareView}
43
43
  pageViewContext={pageViewContext}
44
44
  />
45
45
  ))}{" "}
@@ -2,19 +2,23 @@ import { useEffect, useRef } from "react";
2
2
  import {
3
3
  useEditContext,
4
4
  useModifiedFieldsContext,
5
+ useEditContextRef,
5
6
  } from "../client/editContext";
6
7
  import { useThrottledCallback } from "use-debounce";
7
8
  import { getFieldDescriptorFromElement, hasFieldLock } from "../utils";
8
9
  import { PageViewContext } from "../page-viewer/pageViewContext";
10
+ import { applyPatch, convertChangesToXML, diffWords } from "diff";
11
+ import { createPatch } from "diff";
9
12
 
10
13
  export function InlineEditor({
11
14
  pageViewContext,
12
- mode,
15
+ compareView,
13
16
  }: {
14
17
  pageViewContext: PageViewContext;
15
- mode: "edit" | "view";
18
+ compareView: boolean;
16
19
  }) {
17
20
  const context = useEditContext();
21
+ const contextRef = useEditContextRef();
18
22
  const modifiedFieldsContext = useModifiedFieldsContext();
19
23
 
20
24
  if (!context) return;
@@ -33,7 +37,7 @@ export function InlineEditor({
33
37
 
34
38
  if (modifiedFieldValue === value) return;
35
39
 
36
- context.operations.editField({
40
+ contextRef.current?.operations.editField({
37
41
  field: {
38
42
  fieldId,
39
43
  fieldName: fieldName ?? undefined,
@@ -51,7 +55,7 @@ export function InlineEditor({
51
55
  );
52
56
 
53
57
  useEffect(() => {
54
- if (!context || mode === "view") return;
58
+ if (!context || compareView || context.mode === "preview") return;
55
59
  const element = context.inlineEditingFieldElement;
56
60
 
57
61
  const editableElements =
@@ -111,37 +115,244 @@ export function InlineEditor({
111
115
  };
112
116
  }, [context?.inlineEditingFieldElement]);
113
117
 
118
+ function saveCaretPosition(editableElement: HTMLElement): Range | null {
119
+ const selection =
120
+ pageViewContext.editorIframeRef.current?.contentWindow?.getSelection();
121
+ if (
122
+ selection &&
123
+ selection.rangeCount > 0 &&
124
+ editableElement.contains(selection.anchorNode)
125
+ ) {
126
+ return selection.getRangeAt(0);
127
+ }
128
+ return null;
129
+ }
130
+
131
+ function restoreCaretPosition(range: Range | null) {
132
+ if (range) {
133
+ const selection =
134
+ pageViewContext.editorIframeRef.current?.contentWindow?.getSelection();
135
+ selection?.removeAllRanges();
136
+ selection?.addRange(range);
137
+ }
138
+ }
139
+
114
140
  useEffect(() => {
115
- const updateFields = async () => {
116
- modifiedFieldsContext?.modifiedFields.forEach((field) => {
117
- const elements =
118
- pageViewContext.editorIframeRef.current!.contentWindow?.document.querySelectorAll(
119
- `[data-fieldid="${field.fieldId}"][data-itemid="${field.item.id}"][data-language="${field.item.language}"][data-version="${field.item.version}"`,
141
+ const updateFieldsWithSuggestions = async () => {
142
+ const iframeWindow =
143
+ pageViewContext.editorIframeRef.current?.contentWindow;
144
+ if (!iframeWindow) return;
145
+ const doc = iframeWindow.document;
146
+
147
+ // Query all field elements by data attributes.
148
+ const fieldElements = doc.querySelectorAll(
149
+ "[data-fieldid][data-itemid][data-language][data-version]",
150
+ );
151
+
152
+ fieldElements.forEach(async (element) => {
153
+ // Do not update if this field is currently focused.
154
+ if (element === context.inlineEditingFieldElement) return;
155
+
156
+ const fieldId = element.getAttribute("data-fieldid");
157
+ const itemId = element.getAttribute("data-itemid");
158
+ const language = element.getAttribute("data-language");
159
+ const versionStr = element.getAttribute("data-version");
160
+ if (!fieldId || !itemId || !language || !versionStr) return;
161
+ const version = parseInt(versionStr, 10);
162
+
163
+ // Build an item descriptor.
164
+ const descriptor = { id: itemId, language, version };
165
+
166
+ // Fetch the current item from the repository.
167
+ const loadedItem = await context.itemsRepository.getItem(descriptor);
168
+ if (!loadedItem) return;
169
+
170
+ // Get the baseline from repository.
171
+ const repositoryField = loadedItem.fields.find(
172
+ (f: any) => f.id === fieldId,
173
+ );
174
+ let originalValue = repositoryField
175
+ ? repositoryField.rawValue || ""
176
+ : "";
177
+
178
+ // If the field is modified locally, use that value.
179
+ const modField = modifiedFieldsContext?.modifiedFields.find(
180
+ (mod: any) =>
181
+ mod.fieldId === fieldId &&
182
+ mod.item.id === itemId &&
183
+ mod.item.language === language &&
184
+ mod.item.version === version,
185
+ );
186
+ if (modField) {
187
+ originalValue = modField.value || "";
188
+ }
189
+
190
+ // If showSuggestedEdits is false, update with the base value.
191
+ if (!context.showSuggestedEdits) {
192
+ element.innerHTML = originalValue;
193
+ return;
194
+ }
195
+
196
+ // Otherwise, gather all suggestions for this field.
197
+ const fieldSuggestions = context.suggestedEdits.filter(
198
+ (s: any) =>
199
+ s.fieldId === fieldId &&
200
+ s.itemId === itemId &&
201
+ s.mainItemLanguage === language &&
202
+ s.mainItemId === pageViewContext.pageItemDescriptor?.id &&
203
+ s.mainItemVersion === version,
204
+ );
205
+
206
+ // Sort suggestions in chronological order (oldest first).
207
+ fieldSuggestions.sort(
208
+ (a: any, b: any) =>
209
+ new Date(a.created).getTime() - new Date(b.created).getTime(),
210
+ );
211
+
212
+ // Apply suggestions sequentially to generate the merged value.
213
+ let mergedValue = originalValue;
214
+ for (const suggestion of fieldSuggestions) {
215
+ // Compute a patch from the suggestion's baseline to its intended new value.
216
+ const patch = createPatch(
217
+ "field",
218
+ suggestion.oldValue,
219
+ suggestion.newValue,
120
220
  );
221
+ const patchedCandidate = applyPatch(mergedValue, patch);
222
+ if (
223
+ patchedCandidate !== false &&
224
+ typeof patchedCandidate === "string"
225
+ ) {
226
+ mergedValue = patchedCandidate;
227
+ }
228
+ // If a patch fails, we simply skip that suggestion.
229
+ }
230
+
231
+ // If showSuggestedEditsDiff is false, show only the merged text
232
+ if (!context.showSuggestedEditsDiff) {
233
+ element.innerHTML = mergedValue;
234
+ return;
235
+ }
121
236
 
122
- elements?.forEach(async (element) => {
123
- if (element && element !== context?.inlineEditingFieldElement) {
124
- const realField = await context.itemsRepository.getField(field);
125
- const fieldType = realField?.type;
126
-
127
- if (
128
- fieldType === "rich text" ||
129
- fieldType === "single-line text" ||
130
- fieldType === "multi-line text"
131
- ) {
132
- element.innerHTML = field?.value ? (field.value as string) : "";
133
- }
237
+ // Compute a word-based diff between originalValue and mergedValue.
238
+ const diffParts = diffWords(originalValue, mergedValue);
239
+
240
+ // Build HTML markup from the diff:
241
+ let diffHTML = "";
242
+ diffParts.forEach((part: any) => {
243
+ let style = "";
244
+ if (part.added) {
245
+ style = "color: green;";
246
+ } else if (part.removed) {
247
+ style = "color: red; text-decoration: line-through;";
248
+ } else {
249
+ style = "color: gray;";
134
250
  }
251
+ // Escape any HTML in part.value if needed.
252
+ diffHTML += `<span style="${style}">${part.value}</span>`;
135
253
  });
254
+
255
+ // Update the element's innerHTML with the diff markup.
256
+ element.innerHTML = diffHTML;
136
257
  });
137
258
  };
138
259
 
139
- updateFields();
260
+ updateFieldsWithSuggestions();
140
261
  }, [
141
262
  modifiedFieldsContext?.modifiedFields,
142
263
  context?.itemsRepository.revision,
143
264
  context?.inlineEditingFieldElement,
265
+ context?.showSuggestedEdits,
266
+ context?.suggestedEdits,
267
+ pageViewContext.pageItemDescriptor,
268
+ context.pageView.page,
269
+ context.showSuggestedEditsDiff,
144
270
  ]);
145
271
 
272
+ useEffect(() => {
273
+ async function updateFocusedFieldContent() {
274
+ const element = context?.inlineEditingFieldElement;
275
+ if (!element) return;
276
+
277
+ const savedRange = saveCaretPosition(element);
278
+ console.log("savedRange", savedRange, element);
279
+
280
+ const fieldId = element.getAttribute("data-fieldid");
281
+ const itemId = element.getAttribute("data-itemid");
282
+ const language = element.getAttribute("data-language");
283
+ const versionStr = element.getAttribute("data-version");
284
+ if (!fieldId || !itemId || !language || !versionStr) return;
285
+
286
+ const version = parseInt(versionStr, 10);
287
+ const descriptor = { id: itemId, language, version };
288
+
289
+ // Retrieve the current field value from the repository.
290
+ const loadedItem = await context.itemsRepository.getItem(descriptor);
291
+ if (!loadedItem) return;
292
+ // Get the baseline value from the repository.
293
+ const repositoryField = loadedItem.fields.find(
294
+ (f: any) => f.id === fieldId,
295
+ );
296
+ let baseValue = repositoryField ? repositoryField.rawValue || "" : "";
297
+
298
+ // Override with the modified field value if it exists.
299
+ const modField = modifiedFieldsContext?.modifiedFields.find(
300
+ (mod: any) =>
301
+ mod.fieldId === fieldId &&
302
+ mod.item.id === itemId &&
303
+ mod.item.language === language &&
304
+ mod.item.version === version,
305
+ );
306
+ if (modField) {
307
+ baseValue = modField.value || "";
308
+ }
309
+
310
+ // If suggestions mode is active, merge all suggestions for this field.
311
+
312
+ if (context.showSuggestedEdits || context.mode === "suggestions") {
313
+ const fieldSuggestions = context.suggestedEdits.filter(
314
+ (s: any) =>
315
+ s.fieldId === fieldId &&
316
+ s.itemId === itemId &&
317
+ s.mainItemLanguage === language &&
318
+ s.mainItemVersion === version,
319
+ );
320
+ // Sort suggestions in chronological order (oldest first).
321
+ fieldSuggestions.sort(
322
+ (a: any, b: any) =>
323
+ new Date(a.created).getTime() - new Date(b.created).getTime(),
324
+ );
325
+
326
+ let mergedValue = baseValue;
327
+ for (const suggestion of fieldSuggestions) {
328
+ const patch = createPatch(
329
+ "field",
330
+ suggestion.oldValue,
331
+ suggestion.newValue,
332
+ );
333
+ const patchedCandidate = applyPatch(mergedValue, patch);
334
+ if (
335
+ patchedCandidate !== false &&
336
+ typeof patchedCandidate === "string"
337
+ ) {
338
+ mergedValue = patchedCandidate;
339
+ }
340
+ }
341
+ // Update focused field element with the merged plain text value.
342
+ element.innerHTML = mergedValue;
343
+ } else {
344
+ if (element.innerHTML !== baseValue) {
345
+ // If suggestions mode is off, update with the base value.
346
+ element.innerHTML = baseValue;
347
+ }
348
+ }
349
+
350
+ console.log("restoring caret position", savedRange);
351
+ restoreCaretPosition(savedRange);
352
+ }
353
+
354
+ updateFocusedFieldContent();
355
+ }, [context?.inlineEditingFieldElement]);
356
+
146
357
  return null;
147
358
  }