@alpaca-editor/core 1.0.4018 → 1.0.4027
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.
- package/dist/components/SimpleLanguageSelector.d.ts +2 -1
- package/dist/components/SimpleLanguageSelector.js +9 -2
- package/dist/components/SimpleLanguageSelector.js.map +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/tooltip.d.ts +3 -1
- package/dist/components/ui/tooltip.js +2 -2
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/config/config.js +4 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/editor/ContentTree.js +1 -1
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/ContextMenu.js +26 -0
- package/dist/editor/ContextMenu.js.map +1 -1
- package/dist/editor/FieldHistory.js +1 -1
- package/dist/editor/FieldHistory.js.map +1 -1
- package/dist/editor/FieldListField.js +2 -2
- package/dist/editor/FieldListField.js.map +1 -1
- package/dist/editor/Terminal.js +3 -1
- package/dist/editor/Terminal.js.map +1 -1
- package/dist/editor/ai/Agents.js +37 -18
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/ai/AiResponseMessage.js +71 -5
- package/dist/editor/ai/AiResponseMessage.js.map +1 -1
- package/dist/editor/ai/AiTerminal.d.ts +3 -1
- package/dist/editor/ai/AiTerminal.js +53 -24
- package/dist/editor/ai/AiTerminal.js.map +1 -1
- package/dist/editor/ai/AiToolCall.js +3 -3
- package/dist/editor/ai/AiToolCall.js.map +1 -1
- package/dist/editor/ai/EditorAiTerminal.d.ts +2 -1
- package/dist/editor/ai/EditorAiTerminal.js +2 -2
- package/dist/editor/ai/EditorAiTerminal.js.map +1 -1
- package/dist/editor/client/EditorClient.js +5 -1
- package/dist/editor/client/EditorClient.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +2 -0
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/operations.d.ts +1 -0
- package/dist/editor/client/operations.js +7 -0
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +1 -1
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/field-types/ImageFieldEditor.js +1 -1
- package/dist/editor/field-types/ImageFieldEditor.js.map +1 -1
- package/dist/editor/field-types/MultiLineText.js +1 -1
- package/dist/editor/field-types/MultiLineText.js.map +1 -1
- package/dist/editor/field-types/PictureFieldEditor.js +1 -1
- package/dist/editor/field-types/PictureFieldEditor.js.map +1 -1
- package/dist/editor/field-types/RawEditor.js +1 -1
- package/dist/editor/field-types/RawEditor.js.map +1 -1
- package/dist/editor/field-types/RichTextEditorComponent.js +16 -17
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/SingleLineText.js +1 -1
- package/dist/editor/field-types/SingleLineText.js.map +1 -1
- package/dist/editor/field-types/richtext/components/SimpleDropdown.d.ts +18 -0
- package/dist/editor/field-types/richtext/components/SimpleDropdown.js +71 -0
- package/dist/editor/field-types/richtext/components/SimpleDropdown.js.map +1 -0
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.d.ts +5 -0
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.js +359 -0
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.js.map +1 -0
- package/dist/editor/field-types/richtext/components/SimpleToolbar.d.ts +16 -0
- package/dist/editor/field-types/richtext/components/SimpleToolbar.js +181 -0
- package/dist/editor/field-types/richtext/components/SimpleToolbar.js.map +1 -0
- package/dist/editor/field-types/richtext/components/SimpleToolbarButton.d.ts +9 -0
- package/dist/editor/field-types/richtext/components/SimpleToolbarButton.js +14 -0
- package/dist/editor/field-types/richtext/components/SimpleToolbarButton.js.map +1 -0
- package/dist/editor/field-types/richtext/contextMenuFactory.d.ts +4 -0
- package/dist/editor/field-types/richtext/contextMenuFactory.js +193 -0
- package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -0
- package/dist/editor/field-types/richtext/index.d.ts +6 -5
- package/dist/editor/field-types/richtext/index.js +6 -5
- package/dist/editor/field-types/richtext/index.js.map +1 -1
- package/dist/editor/field-types/richtext/types.d.ts +16 -16
- package/dist/editor/field-types/richtext/types.js +84 -84
- package/dist/editor/field-types/richtext/types.js.map +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlighting.js +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +5 -5
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +3 -2
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +14 -4
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/aiService.d.ts +1 -0
- package/dist/editor/services/aiService.js +1 -0
- package/dist/editor/services/aiService.js.map +1 -1
- package/dist/page-wizard/PageWizard.d.ts +2 -0
- package/dist/page-wizard/PageWizard.js +6 -13
- package/dist/page-wizard/PageWizard.js.map +1 -1
- package/dist/page-wizard/WizardSteps.js +3 -1
- package/dist/page-wizard/WizardSteps.js.map +1 -1
- package/dist/page-wizard/service.d.ts +1 -0
- package/dist/page-wizard/service.js +7 -0
- package/dist/page-wizard/service.js.map +1 -1
- package/dist/page-wizard/steps/ContentStep.js +210 -33
- package/dist/page-wizard/steps/ContentStep.js.map +1 -1
- package/dist/page-wizard/steps/FindItemsStep.js +11 -3
- package/dist/page-wizard/steps/FindItemsStep.js.map +1 -1
- package/dist/page-wizard/steps/LayoutStep.js +1 -1
- package/dist/page-wizard/steps/LayoutStep.js.map +1 -1
- package/dist/page-wizard/steps/MetaDataStep.js +1 -1
- package/dist/page-wizard/steps/MetaDataStep.js.map +1 -1
- package/dist/page-wizard/steps/SchottSelectImagesStep.d.ts +2 -0
- package/dist/page-wizard/steps/SchottSelectImagesStep.js +55 -0
- package/dist/page-wizard/steps/SchottSelectImagesStep.js.map +1 -0
- package/dist/page-wizard/steps/StructureStep.js +20 -5
- package/dist/page-wizard/steps/StructureStep.js.map +1 -1
- package/dist/page-wizard/steps/TranslateStep.d.ts +2 -0
- package/dist/page-wizard/steps/TranslateStep.js +413 -0
- package/dist/page-wizard/steps/TranslateStep.js.map +1 -0
- package/dist/page-wizard/utils/dataAccessor.d.ts +7 -0
- package/dist/page-wizard/utils/dataAccessor.js +76 -0
- package/dist/page-wizard/utils/dataAccessor.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/splash-screen/NewPage.js +5 -4
- package/dist/splash-screen/NewPage.js.map +1 -1
- package/dist/splash-screen/OpenPage.js +2 -1
- package/dist/splash-screen/OpenPage.js.map +1 -1
- package/dist/splash-screen/RecentPages.js +3 -1
- package/dist/splash-screen/RecentPages.js.map +1 -1
- package/dist/styles.css +57 -0
- package/package.json +5 -4
- package/src/components/SimpleLanguageSelector.tsx +11 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/tooltip.tsx +3 -2
- package/src/config/config.tsx +4 -0
- package/src/config/types.ts +6 -0
- package/src/editor/ContentTree.tsx +1 -1
- package/src/editor/ContextMenu.tsx +39 -0
- package/src/editor/FieldHistory.tsx +1 -1
- package/src/editor/FieldListField.tsx +2 -6
- package/src/editor/Terminal.tsx +5 -1
- package/src/editor/ai/Agents.tsx +90 -46
- package/src/editor/ai/AiResponseMessage.tsx +185 -24
- package/src/editor/ai/AiTerminal.tsx +104 -50
- package/src/editor/ai/AiToolCall.tsx +14 -4
- package/src/editor/ai/EditorAiTerminal.tsx +3 -0
- package/src/editor/client/EditorClient.tsx +9 -1
- package/src/editor/client/editContext.ts +2 -0
- package/src/editor/client/operations.ts +9 -0
- package/src/editor/commands/componentCommands.tsx +1 -1
- package/src/editor/field-types/ImageFieldEditor.tsx +1 -0
- package/src/editor/field-types/MultiLineText.tsx +1 -0
- package/src/editor/field-types/PictureFieldEditor.tsx +1 -0
- package/src/editor/field-types/RawEditor.tsx +1 -0
- package/src/editor/field-types/RichTextEditorComponent.tsx +27 -25
- package/src/editor/field-types/SingleLineText.tsx +1 -0
- package/src/editor/field-types/richtext/components/SimpleDropdown.tsx +165 -0
- package/src/editor/field-types/richtext/components/SimpleRichTextEditor.css +261 -0
- package/src/editor/field-types/richtext/components/SimpleRichTextEditor.tsx +505 -0
- package/src/editor/field-types/richtext/components/SimpleToolbar.tsx +362 -0
- package/src/editor/field-types/richtext/components/SimpleToolbarButton.tsx +36 -0
- package/src/editor/field-types/richtext/contextMenuFactory.tsx +264 -0
- package/src/editor/field-types/richtext/index.ts +6 -5
- package/src/editor/field-types/richtext/types.ts +168 -148
- package/src/editor/page-editor-chrome/CommentHighlighting.tsx +1 -1
- package/src/editor/page-editor-chrome/FrameMenu.tsx +16 -11
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +1 -1
- package/src/editor/page-viewer/PageViewerFrame.tsx +4 -3
- package/src/editor/services/agentService.ts +16 -5
- package/src/editor/services/aiService.ts +4 -0
- package/src/page-wizard/PageWizard.tsx +10 -13
- package/src/page-wizard/WizardSteps.tsx +3 -1
- package/src/page-wizard/service.ts +11 -0
- package/src/page-wizard/steps/ContentStep.tsx +376 -43
- package/src/page-wizard/steps/FindItemsStep.tsx +23 -3
- package/src/page-wizard/steps/LayoutStep.tsx +1 -1
- package/src/page-wizard/steps/MetaDataStep.tsx +1 -1
- package/src/page-wizard/steps/SchottSelectImagesStep.tsx +141 -0
- package/src/page-wizard/steps/StructureStep.tsx +40 -5
- package/src/page-wizard/steps/TranslateStep.tsx +772 -0
- package/src/page-wizard/utils/dataAccessor.ts +85 -0
- package/src/revision.ts +2 -2
- package/src/splash-screen/NewPage.tsx +18 -3
- package/src/splash-screen/OpenPage.tsx +14 -1
- package/src/splash-screen/RecentPages.tsx +4 -2
- package/tsconfig.build.json +1 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
RichTextEditorProfile,
|
|
4
|
+
ToolbarGroupConfig,
|
|
5
|
+
ToolbarOptionConfig,
|
|
6
|
+
SLATE_MARKS,
|
|
7
|
+
SLATE_BLOCKS,
|
|
8
|
+
SLATE_ALIGNMENTS,
|
|
9
|
+
} from "../types";
|
|
10
|
+
import { SimpleToolbarButton } from "./SimpleToolbarButton";
|
|
11
|
+
import { SimpleDropdown } from "./SimpleDropdown";
|
|
12
|
+
|
|
13
|
+
interface SimpleToolbarProps {
|
|
14
|
+
profile: RichTextEditorProfile;
|
|
15
|
+
isMarkActive: (markId: string) => boolean;
|
|
16
|
+
isBlockActive: (blockId: string) => boolean;
|
|
17
|
+
isAlignActive: (alignment: string) => boolean;
|
|
18
|
+
isListActive: (listType: "unordered" | "ordered") => boolean;
|
|
19
|
+
toggleMark: (markId: string) => void;
|
|
20
|
+
toggleBlock: (blockId: string) => void;
|
|
21
|
+
toggleAlign: (alignment: string) => void;
|
|
22
|
+
toggleList: (listType: "unordered" | "ordered") => void;
|
|
23
|
+
stripFormatting: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ToolbarOption {
|
|
27
|
+
id: string;
|
|
28
|
+
type: string;
|
|
29
|
+
label: string;
|
|
30
|
+
icon: React.ReactNode;
|
|
31
|
+
isActive: boolean;
|
|
32
|
+
onSelect: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SimpleToolbar: React.FC<SimpleToolbarProps> = ({
|
|
36
|
+
profile,
|
|
37
|
+
isMarkActive,
|
|
38
|
+
isBlockActive,
|
|
39
|
+
isAlignActive,
|
|
40
|
+
isListActive,
|
|
41
|
+
toggleMark,
|
|
42
|
+
toggleBlock,
|
|
43
|
+
toggleAlign,
|
|
44
|
+
toggleList,
|
|
45
|
+
stripFormatting,
|
|
46
|
+
}) => {
|
|
47
|
+
// Create toolbar option from configuration
|
|
48
|
+
const createToolbarOption = useCallback(
|
|
49
|
+
(option: ToolbarOptionConfig): ToolbarOption | null => {
|
|
50
|
+
switch (option.type) {
|
|
51
|
+
case "mark":
|
|
52
|
+
const markConfig = SLATE_MARKS[option.id];
|
|
53
|
+
if (markConfig) {
|
|
54
|
+
return {
|
|
55
|
+
id: option.id,
|
|
56
|
+
type: option.type,
|
|
57
|
+
label: markConfig.label,
|
|
58
|
+
icon: markConfig.icon,
|
|
59
|
+
isActive: isMarkActive(option.id),
|
|
60
|
+
onSelect: () => toggleMark(option.id),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
|
|
65
|
+
case "block":
|
|
66
|
+
const blockConfig = SLATE_BLOCKS[option.id];
|
|
67
|
+
if (blockConfig) {
|
|
68
|
+
return {
|
|
69
|
+
id: option.id,
|
|
70
|
+
type: option.type,
|
|
71
|
+
label: blockConfig.label,
|
|
72
|
+
icon: blockConfig.icon,
|
|
73
|
+
isActive: isBlockActive(option.id),
|
|
74
|
+
onSelect: () => toggleBlock(option.id),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
|
|
79
|
+
case "alignment":
|
|
80
|
+
const alignConfig = SLATE_ALIGNMENTS[option.id];
|
|
81
|
+
if (alignConfig) {
|
|
82
|
+
return {
|
|
83
|
+
id: option.id,
|
|
84
|
+
type: option.type,
|
|
85
|
+
label: alignConfig.label,
|
|
86
|
+
icon: alignConfig.icon,
|
|
87
|
+
isActive: isAlignActive(alignConfig.value),
|
|
88
|
+
onSelect: () => toggleAlign(alignConfig.value),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
|
|
93
|
+
case "list":
|
|
94
|
+
const listType =
|
|
95
|
+
option.id === "unordered-list" ? "unordered" : "ordered";
|
|
96
|
+
return {
|
|
97
|
+
id: option.id,
|
|
98
|
+
type: option.type,
|
|
99
|
+
label:
|
|
100
|
+
option.id === "unordered-list"
|
|
101
|
+
? "Bulleted List"
|
|
102
|
+
: "Numbered List",
|
|
103
|
+
icon: option.id === "unordered-list" ? "•" : "1.",
|
|
104
|
+
isActive: isListActive(listType),
|
|
105
|
+
onSelect: () => toggleList(listType),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
case "divider":
|
|
109
|
+
return {
|
|
110
|
+
id: "divider",
|
|
111
|
+
type: "divider",
|
|
112
|
+
label: "Divider",
|
|
113
|
+
icon: null,
|
|
114
|
+
isActive: false,
|
|
115
|
+
onSelect: () => {},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
case "strip":
|
|
119
|
+
return {
|
|
120
|
+
id: "strip-formatting",
|
|
121
|
+
type: "strip",
|
|
122
|
+
label: "Strip Formatting",
|
|
123
|
+
icon: "✗",
|
|
124
|
+
isActive: false,
|
|
125
|
+
onSelect: () => stripFormatting(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[
|
|
133
|
+
isMarkActive,
|
|
134
|
+
isBlockActive,
|
|
135
|
+
isAlignActive,
|
|
136
|
+
isListActive,
|
|
137
|
+
toggleMark,
|
|
138
|
+
toggleBlock,
|
|
139
|
+
toggleAlign,
|
|
140
|
+
toggleList,
|
|
141
|
+
],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Split options by dividers
|
|
145
|
+
const splitOptionsByDividers = (
|
|
146
|
+
options: ToolbarOption[],
|
|
147
|
+
): ToolbarOption[][] => {
|
|
148
|
+
const groups: ToolbarOption[][] = [];
|
|
149
|
+
let currentGroup: ToolbarOption[] = [];
|
|
150
|
+
|
|
151
|
+
options.forEach((option) => {
|
|
152
|
+
if (option.type === "divider") {
|
|
153
|
+
if (currentGroup.length > 0) {
|
|
154
|
+
groups.push(currentGroup);
|
|
155
|
+
currentGroup = [];
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
currentGroup.push(option);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (currentGroup.length > 0) {
|
|
163
|
+
groups.push(currentGroup);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return groups;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Render a toolbar group
|
|
170
|
+
const renderToolbarGroup = (group: ToolbarGroupConfig, rowIndex: number) => {
|
|
171
|
+
if (!group.options || group.options.length === 0) return null;
|
|
172
|
+
|
|
173
|
+
// Create toolbar options from configuration
|
|
174
|
+
const validOptions = group.options
|
|
175
|
+
.map(createToolbarOption)
|
|
176
|
+
.filter((option): option is ToolbarOption => option !== null);
|
|
177
|
+
|
|
178
|
+
if (validOptions.length === 0) return null;
|
|
179
|
+
|
|
180
|
+
const groupKey = `${group.id}-${rowIndex}`;
|
|
181
|
+
|
|
182
|
+
// Render as buttons
|
|
183
|
+
if (group.display === "buttons" || !group.display) {
|
|
184
|
+
const subGroups = splitOptionsByDividers(validOptions);
|
|
185
|
+
return (
|
|
186
|
+
<div key={groupKey} className="toolbar-group">
|
|
187
|
+
{subGroups.map((subGroup, subGroupIndex) => (
|
|
188
|
+
<div
|
|
189
|
+
key={`subgroup-${subGroupIndex}`}
|
|
190
|
+
className="toolbar-button-group"
|
|
191
|
+
>
|
|
192
|
+
{subGroup.map((option) => (
|
|
193
|
+
<SimpleToolbarButton
|
|
194
|
+
key={`${option.type}-${option.id}`}
|
|
195
|
+
icon={option.icon}
|
|
196
|
+
active={option.isActive}
|
|
197
|
+
title={option.label}
|
|
198
|
+
onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
option.onSelect();
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Render as dropdown
|
|
211
|
+
if (group.display === "dropdown") {
|
|
212
|
+
const blockOptions = validOptions.filter(
|
|
213
|
+
(option) => option.type === "block",
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// If there's only one block option, render it as a disabled-style button
|
|
217
|
+
if (blockOptions.length === 1) {
|
|
218
|
+
const singleOption = blockOptions[0];
|
|
219
|
+
if (!singleOption) return null;
|
|
220
|
+
return (
|
|
221
|
+
<div key={groupKey} className="toolbar-group">
|
|
222
|
+
<div className="toolbar-dropdown-container">
|
|
223
|
+
<button className="toolbar-dropdown-button" disabled>
|
|
224
|
+
{group.label ? (
|
|
225
|
+
<>
|
|
226
|
+
<span className="toolbar-dropdown-content">
|
|
227
|
+
{group.label}: {singleOption.label}
|
|
228
|
+
</span>
|
|
229
|
+
<span className="toolbar-dropdown-arrow">▼</span>
|
|
230
|
+
</>
|
|
231
|
+
) : group.showIconsOnly ? (
|
|
232
|
+
<>
|
|
233
|
+
<span className="toolbar-dropdown-icon">
|
|
234
|
+
{singleOption.icon}
|
|
235
|
+
</span>
|
|
236
|
+
<span className="toolbar-dropdown-arrow">▼</span>
|
|
237
|
+
</>
|
|
238
|
+
) : (
|
|
239
|
+
<>
|
|
240
|
+
<span className="toolbar-dropdown-icon">
|
|
241
|
+
{singleOption.icon && (
|
|
242
|
+
<span className="toolbar-dropdown-icon">
|
|
243
|
+
{singleOption.icon}
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
<span className="toolbar-dropdown-content">
|
|
247
|
+
{singleOption.label}
|
|
248
|
+
</span>
|
|
249
|
+
</span>
|
|
250
|
+
<span className="toolbar-dropdown-arrow">▼</span>
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Multiple options - render normally with dropdown
|
|
260
|
+
const isActive = validOptions.some((option) => option.isActive);
|
|
261
|
+
return (
|
|
262
|
+
<div key={groupKey} className="toolbar-group">
|
|
263
|
+
<div className="toolbar-dropdown-container">
|
|
264
|
+
<SimpleDropdown
|
|
265
|
+
options={validOptions}
|
|
266
|
+
label={group.label}
|
|
267
|
+
showIconsOnly={group.showIconsOnly}
|
|
268
|
+
isActive={isActive}
|
|
269
|
+
title={
|
|
270
|
+
group.label
|
|
271
|
+
? `${group.label} formatting options`
|
|
272
|
+
: "Formatting options"
|
|
273
|
+
}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (!profile.toolbar?.groups || profile.toolbar.groups.length === 0) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Group toolbar items by row
|
|
288
|
+
const groupsByRow = profile.toolbar.groups.reduce(
|
|
289
|
+
(acc, group, index) => {
|
|
290
|
+
const row = group.row !== undefined ? group.row : index;
|
|
291
|
+
if (!acc[row]) acc[row] = [];
|
|
292
|
+
acc[row].push(group);
|
|
293
|
+
return acc;
|
|
294
|
+
},
|
|
295
|
+
{} as Record<number, typeof profile.toolbar.groups>,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Create strip formatting option that's always available
|
|
299
|
+
const stripFormattingOption: ToolbarOption = {
|
|
300
|
+
id: "strip-formatting",
|
|
301
|
+
type: "strip",
|
|
302
|
+
label: "Strip Formatting",
|
|
303
|
+
icon: "✗",
|
|
304
|
+
isActive: false,
|
|
305
|
+
onSelect: () => stripFormatting(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Render each row
|
|
309
|
+
return (
|
|
310
|
+
<div className="toolbar">
|
|
311
|
+
{Object.entries(groupsByRow)
|
|
312
|
+
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
|
313
|
+
.map(([rowIndex, rowGroups]) => (
|
|
314
|
+
<div key={`row-${rowIndex}`} className="toolbar-row">
|
|
315
|
+
{rowGroups.map((group) =>
|
|
316
|
+
renderToolbarGroup(group, parseInt(rowIndex)),
|
|
317
|
+
)}
|
|
318
|
+
{/* Add strip formatting button to the first row */}
|
|
319
|
+
{parseInt(rowIndex) === 0 && (
|
|
320
|
+
<div className="toolbar-group">
|
|
321
|
+
<div className="toolbar-button-group">
|
|
322
|
+
<SimpleToolbarButton
|
|
323
|
+
key="strip-formatting"
|
|
324
|
+
icon={stripFormattingOption.icon}
|
|
325
|
+
active={stripFormattingOption.isActive}
|
|
326
|
+
title={stripFormattingOption.label}
|
|
327
|
+
onMouseDown={(
|
|
328
|
+
event: React.MouseEvent<HTMLButtonElement>,
|
|
329
|
+
) => {
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
stripFormattingOption.onSelect();
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
))}
|
|
339
|
+
{/* If no groups exist, still show strip formatting */}
|
|
340
|
+
{Object.keys(groupsByRow).length === 0 && (
|
|
341
|
+
<div className="toolbar-row">
|
|
342
|
+
<div className="toolbar-group">
|
|
343
|
+
<div className="toolbar-button-group">
|
|
344
|
+
<SimpleToolbarButton
|
|
345
|
+
key="strip-formatting"
|
|
346
|
+
icon={stripFormattingOption.icon}
|
|
347
|
+
active={stripFormattingOption.isActive}
|
|
348
|
+
title={stripFormattingOption.label}
|
|
349
|
+
onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
stripFormattingOption.onSelect();
|
|
352
|
+
}}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export default SimpleToolbar;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface SimpleToolbarButtonProps {
|
|
4
|
+
icon: React.ReactNode;
|
|
5
|
+
active: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
onMouseDown?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const SimpleToolbarButton: React.FC<SimpleToolbarButtonProps> = ({
|
|
11
|
+
icon,
|
|
12
|
+
active,
|
|
13
|
+
title,
|
|
14
|
+
onMouseDown,
|
|
15
|
+
}) => {
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
className={`toolbar-button ${active ? "active" : ""}`}
|
|
19
|
+
onMouseDown={onMouseDown}
|
|
20
|
+
title={title}
|
|
21
|
+
style={{
|
|
22
|
+
padding: "5px 10px",
|
|
23
|
+
margin: "0 2px",
|
|
24
|
+
background: active ? "#ffffff" : "transparent",
|
|
25
|
+
border: "none",
|
|
26
|
+
borderRadius: "3px",
|
|
27
|
+
cursor: "pointer",
|
|
28
|
+
boxShadow: active ? "0 1px 2px rgba(0,0,0,0.1)" : "none",
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{icon}
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default SimpleToolbarButton;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { MenuItem } from "../../../config/types";
|
|
2
|
+
import { Field } from "../../pageModel";
|
|
3
|
+
import { EditContextType } from "../../client/editContext";
|
|
4
|
+
import { getRichTextProfile } from "../../services/contentService";
|
|
5
|
+
import { getCachedParsedProfile } from "./utils/profileServiceCache";
|
|
6
|
+
import {
|
|
7
|
+
RichTextEditorProfile,
|
|
8
|
+
SLATE_MARKS,
|
|
9
|
+
SLATE_BLOCKS,
|
|
10
|
+
SLATE_ALIGNMENTS,
|
|
11
|
+
RichTextField,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
// Helper function to execute formatting commands on the contentEditable element in iframe
|
|
15
|
+
function executeFormatCommand(
|
|
16
|
+
command: string,
|
|
17
|
+
iframeDocument: Document,
|
|
18
|
+
value?: string,
|
|
19
|
+
): boolean {
|
|
20
|
+
try {
|
|
21
|
+
// Focus the contentEditable element first
|
|
22
|
+
const activeElement = iframeDocument.activeElement;
|
|
23
|
+
if (
|
|
24
|
+
activeElement &&
|
|
25
|
+
(activeElement as HTMLElement).contentEditable === "true"
|
|
26
|
+
) {
|
|
27
|
+
(activeElement as HTMLElement).focus();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Execute the command on the iframe document
|
|
31
|
+
console.log("Executing command:", command, value);
|
|
32
|
+
return iframeDocument.execCommand(command, false, value);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Failed to execute command: ${command}`, error);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper function to get the current contentEditable element for this field in iframe
|
|
40
|
+
function getFieldEditableElement(
|
|
41
|
+
fieldId: string,
|
|
42
|
+
iframeDocument: Document,
|
|
43
|
+
): HTMLElement | null {
|
|
44
|
+
// Look for the contentEditable element with the matching field ID in iframe
|
|
45
|
+
const element = iframeDocument.querySelector(
|
|
46
|
+
`[data-fieldid="${fieldId}"][contenteditable="true"]`,
|
|
47
|
+
) as HTMLElement;
|
|
48
|
+
return element;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Trigger a save by calling the editContext operations directly
|
|
52
|
+
// Wait for DOM to update before reading content to avoid race conditions
|
|
53
|
+
async function triggerSave(
|
|
54
|
+
element: HTMLElement,
|
|
55
|
+
field: Field,
|
|
56
|
+
editContext: EditContextType,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
// Wait for DOM updates to complete before reading content
|
|
60
|
+
await new Promise((resolve) => {
|
|
61
|
+
// Use a shorter delay - just enough for execCommand to complete
|
|
62
|
+
setTimeout(resolve, 50);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const isRichText = element.getAttribute("data-is-richtext") === "true";
|
|
66
|
+
const value = isRichText ? element.innerHTML : element.innerText;
|
|
67
|
+
|
|
68
|
+
await editContext.operations.editField({
|
|
69
|
+
field: field.descriptor,
|
|
70
|
+
value: value,
|
|
71
|
+
refresh: "none",
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Failed to save field value:", error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function createRichTextContextMenu(
|
|
79
|
+
field: Field,
|
|
80
|
+
editContext: EditContextType,
|
|
81
|
+
): Promise<MenuItem[]> {
|
|
82
|
+
const richTextField = field as RichTextField;
|
|
83
|
+
const profilePath = richTextField.customProperties?.profile;
|
|
84
|
+
|
|
85
|
+
if (!profilePath) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get the iframe document
|
|
90
|
+
const iframeDocument =
|
|
91
|
+
editContext.pageView?.editorIframeRef?.current?.contentWindow?.document;
|
|
92
|
+
if (!iframeDocument) {
|
|
93
|
+
console.error("Could not find iframe document for rich text editing");
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Get the rich text profile using the cached parsed profile
|
|
99
|
+
const profile: RichTextEditorProfile | null = await getCachedParsedProfile(
|
|
100
|
+
profilePath,
|
|
101
|
+
getRichTextProfile,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!profile?.toolbar?.groups) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create format submenu items based on the profile
|
|
109
|
+
const formatMenuItems: MenuItem[] = [];
|
|
110
|
+
|
|
111
|
+
// Process each toolbar group to extract formatting options
|
|
112
|
+
for (const group of profile.toolbar.groups) {
|
|
113
|
+
if (group.options && group.options.length > 0) {
|
|
114
|
+
// Collect valid items for this group first
|
|
115
|
+
const groupItems: MenuItem[] = [];
|
|
116
|
+
|
|
117
|
+
// Process each option in the group
|
|
118
|
+
for (const option of group.options) {
|
|
119
|
+
let menuItem: MenuItem | null = null;
|
|
120
|
+
|
|
121
|
+
switch (option.type) {
|
|
122
|
+
case "mark":
|
|
123
|
+
const markConfig = SLATE_MARKS[option.id];
|
|
124
|
+
if (markConfig) {
|
|
125
|
+
let command = "";
|
|
126
|
+
// Only support specific formatting commands
|
|
127
|
+
switch (option.id) {
|
|
128
|
+
case "bold":
|
|
129
|
+
command = "bold";
|
|
130
|
+
break;
|
|
131
|
+
case "italic":
|
|
132
|
+
command = "italic";
|
|
133
|
+
break;
|
|
134
|
+
case "underline":
|
|
135
|
+
command = "underline";
|
|
136
|
+
break;
|
|
137
|
+
case "strikethrough":
|
|
138
|
+
command = "strikeThrough";
|
|
139
|
+
break;
|
|
140
|
+
case "subscript":
|
|
141
|
+
command = "subscript";
|
|
142
|
+
break;
|
|
143
|
+
case "superscript":
|
|
144
|
+
command = "superscript";
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
// Skip unsupported commands
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
menuItem = {
|
|
152
|
+
id: `format-mark-${option.id}`,
|
|
153
|
+
label: markConfig.label,
|
|
154
|
+
icon: (
|
|
155
|
+
<span
|
|
156
|
+
style={{ fontFamily: "monospace", fontWeight: "bold" }}
|
|
157
|
+
>
|
|
158
|
+
{markConfig.icon}
|
|
159
|
+
</span>
|
|
160
|
+
),
|
|
161
|
+
command: async () => {
|
|
162
|
+
try {
|
|
163
|
+
const element = getFieldEditableElement(
|
|
164
|
+
field.id,
|
|
165
|
+
iframeDocument,
|
|
166
|
+
);
|
|
167
|
+
if (element) {
|
|
168
|
+
element.focus();
|
|
169
|
+
const success = executeFormatCommand(
|
|
170
|
+
command,
|
|
171
|
+
iframeDocument,
|
|
172
|
+
);
|
|
173
|
+
if (success) {
|
|
174
|
+
await triggerSave(element, field, editContext);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(
|
|
179
|
+
`Failed to apply mark formatting: ${option.id}`,
|
|
180
|
+
error,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
// Block, alignment, and list formatting not supported
|
|
189
|
+
case "block":
|
|
190
|
+
case "alignment":
|
|
191
|
+
case "list":
|
|
192
|
+
// Skip unsupported formatting types
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (menuItem) {
|
|
197
|
+
groupItems.push(menuItem);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Only add group items if we have any valid items for this group
|
|
202
|
+
if (groupItems.length > 0) {
|
|
203
|
+
// Add separator if we already have items from previous groups
|
|
204
|
+
if (formatMenuItems.length > 0) {
|
|
205
|
+
formatMenuItems.push({
|
|
206
|
+
id: `${group.id}-separator`,
|
|
207
|
+
separator: true,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Add all valid items from this group
|
|
212
|
+
formatMenuItems.push(...groupItems);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Build the context menu items
|
|
218
|
+
const menuItems: MenuItem[] = [];
|
|
219
|
+
|
|
220
|
+
// Add the Format submenu if we have any items
|
|
221
|
+
if (formatMenuItems.length > 0) {
|
|
222
|
+
menuItems.push({
|
|
223
|
+
id: "format-submenu",
|
|
224
|
+
label: "Format",
|
|
225
|
+
icon: (
|
|
226
|
+
<span style={{ fontFamily: "monospace", fontWeight: "bold" }}>
|
|
227
|
+
F
|
|
228
|
+
</span>
|
|
229
|
+
),
|
|
230
|
+
items: formatMenuItems,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Add Strip Formatting command
|
|
235
|
+
menuItems.push({
|
|
236
|
+
id: "strip-formatting",
|
|
237
|
+
label: "Strip Formatting",
|
|
238
|
+
icon: (
|
|
239
|
+
<span style={{ fontFamily: "monospace", fontWeight: "bold" }}>
|
|
240
|
+
✗
|
|
241
|
+
</span>
|
|
242
|
+
),
|
|
243
|
+
command: async () => {
|
|
244
|
+
try {
|
|
245
|
+
const element = getFieldEditableElement(field.id, iframeDocument);
|
|
246
|
+
if (element) {
|
|
247
|
+
element.focus();
|
|
248
|
+
const success = executeFormatCommand("removeFormat", iframeDocument);
|
|
249
|
+
if (success) {
|
|
250
|
+
await triggerSave(element, field, editContext);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error("Failed to strip formatting:", error);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return menuItems;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error("Error creating RichText context menu:", error);
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
1
|
+
export * from "./components/ReactSlate";
|
|
2
|
+
export * from "./components/SimpleRichTextEditor";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
export * from "./utils/profileServiceCache";
|
|
5
|
+
export * from "./hooks/useProfileCache";
|
|
6
|
+
export * from "./hooks/useRichTextProfile";
|