@bayonai/rich-text-editor 0.1.2 → 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 (151) hide show
  1. package/BEHAVIOR.md +396 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +25 -6
  4. package/dist/core/blockTree.d.ts +14 -0
  5. package/dist/core/blockTree.js +126 -0
  6. package/dist/core/blockTypes.d.ts +6 -0
  7. package/dist/core/blockTypes.js +5 -0
  8. package/dist/core/exportImport.d.ts +59 -0
  9. package/dist/core/exportImport.js +51 -0
  10. package/dist/core/features.d.ts +59 -0
  11. package/dist/core/features.js +57 -0
  12. package/dist/core/imageBlockDiagnostics.d.ts +4 -0
  13. package/dist/core/imageBlockDiagnostics.js +19 -0
  14. package/dist/core/proFeatures.d.ts +60 -0
  15. package/dist/core/proFeatures.js +64 -0
  16. package/dist/{richText.d.ts → core/richText.d.ts} +2 -0
  17. package/dist/core/richText.js +566 -0
  18. package/dist/core/types.d.ts +78 -0
  19. package/dist/index.d.ts +14 -8
  20. package/dist/index.js +8 -5
  21. package/dist/react/editor/RichTextBody.d.ts +28 -0
  22. package/dist/react/editor/RichTextBody.js +131 -0
  23. package/dist/react/editor/RichTextEditor.d.ts +138 -0
  24. package/dist/react/editor/RichTextEditor.js +2925 -0
  25. package/dist/react/editor/RichTextRenderedBlock.d.ts +20 -0
  26. package/dist/react/editor/RichTextRenderedBlock.js +162 -0
  27. package/dist/react/editor/RichTextRenderer.d.ts +13 -0
  28. package/dist/react/editor/RichTextRenderer.js +16 -0
  29. package/dist/react/{RichTextTitleInput.d.ts → editor/RichTextTitleInput.d.ts} +11 -1
  30. package/dist/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +17 -2
  31. package/dist/react/editor/blockActions.d.ts +48 -0
  32. package/dist/react/editor/blockActions.js +495 -0
  33. package/dist/react/editor/editorHistory.d.ts +55 -0
  34. package/dist/react/editor/editorHistory.js +111 -0
  35. package/dist/react/{editorNavigation.d.ts → editor/editorNavigation.d.ts} +2 -0
  36. package/dist/react/{editorNavigation.js → editor/editorNavigation.js} +16 -0
  37. package/dist/react/editor/editorOperations.d.ts +10 -0
  38. package/dist/react/editor/editorOperations.js +3 -0
  39. package/dist/react/editor/editorSelection.d.ts +3 -0
  40. package/dist/react/editor/editorSelection.js +215 -0
  41. package/dist/react/{editorShortcuts.d.ts → editor/editorShortcuts.d.ts} +10 -0
  42. package/dist/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  43. package/dist/react/{RichTextIcons.d.ts → icons/RichTextIcons.d.ts} +3 -0
  44. package/dist/react/{RichTextIcons.js → icons/RichTextIcons.js} +9 -0
  45. package/dist/react/index.d.ts +12 -9
  46. package/dist/react/index.js +7 -6
  47. package/dist/react/{EditorSessionProvider.d.ts → session/EditorSessionProvider.d.ts} +2 -2
  48. package/dist/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  49. package/dist/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  50. package/dist/react/styles/RichTextStyles.js +1362 -0
  51. package/dist/react/{BlockActionTool.d.ts → tools/BlockActionTool.d.ts} +1 -1
  52. package/dist/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  53. package/dist/react/tools/LinkCreationInput.d.ts +9 -0
  54. package/dist/react/tools/LinkCreationInput.js +38 -0
  55. package/dist/react/{SelectionFormatToolbar.d.ts → tools/SelectionFormatToolbar.d.ts} +3 -2
  56. package/dist/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  57. package/dist/react/tools/SpecialBlockOption.d.ts +9 -0
  58. package/dist/react/tools/SpecialBlockOption.js +8 -0
  59. package/dist/react/tools/SpecialBlockTool.d.ts +91 -0
  60. package/dist/react/tools/SpecialBlockTool.js +125 -0
  61. package/dist/react/{TranscriptionControl.d.ts → tools/TranscriptionControl.d.ts} +9 -0
  62. package/dist/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +70 -9
  63. package/dist/react/tools/blockActionToolState.d.ts +41 -0
  64. package/dist/react/tools/blockActionToolState.js +177 -0
  65. package/dist/react/tools/imageBlockDiagnostics.d.ts +2 -0
  66. package/dist/react/tools/imageBlockDiagnostics.js +12 -0
  67. package/dist/{session.d.ts → session/session.d.ts} +1 -1
  68. package/dist-cjs/core/blockTree.js +137 -0
  69. package/dist-cjs/core/blockTypes.js +9 -0
  70. package/dist-cjs/core/exportImport.js +56 -0
  71. package/dist-cjs/core/features.js +62 -0
  72. package/dist-cjs/core/proFeatures.js +70 -0
  73. package/dist-cjs/core/richText.js +578 -0
  74. package/dist-cjs/index.js +22 -6
  75. package/dist-cjs/react/editor/RichTextBody.js +134 -0
  76. package/dist-cjs/react/editor/RichTextEditor.js +2956 -0
  77. package/dist-cjs/react/editor/RichTextRenderedBlock.js +166 -0
  78. package/dist-cjs/react/editor/RichTextRenderer.js +20 -0
  79. package/dist-cjs/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +18 -2
  80. package/dist-cjs/react/editor/blockActions.js +518 -0
  81. package/dist-cjs/react/editor/editorHistory.js +120 -0
  82. package/dist-cjs/react/{editorNavigation.js → editor/editorNavigation.js} +17 -0
  83. package/dist-cjs/react/editor/editorOperations.js +6 -0
  84. package/dist-cjs/react/editor/editorSelection.js +219 -0
  85. package/dist-cjs/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  86. package/dist-cjs/react/{RichTextIcons.js → icons/RichTextIcons.js} +12 -0
  87. package/dist-cjs/react/index.js +9 -7
  88. package/dist-cjs/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  89. package/dist-cjs/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  90. package/dist-cjs/react/styles/RichTextStyles.js +1365 -0
  91. package/dist-cjs/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  92. package/dist-cjs/react/tools/LinkCreationInput.js +41 -0
  93. package/dist-cjs/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  94. package/dist-cjs/react/tools/SpecialBlockOption.js +11 -0
  95. package/dist-cjs/react/tools/SpecialBlockTool.js +129 -0
  96. package/dist-cjs/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +71 -9
  97. package/dist-cjs/react/tools/blockActionToolState.js +186 -0
  98. package/package.json +3 -2
  99. package/dist/react/RichTextBody.d.ts +0 -18
  100. package/dist/react/RichTextBody.js +0 -66
  101. package/dist/react/RichTextEditor.d.ts +0 -45
  102. package/dist/react/RichTextEditor.js +0 -1096
  103. package/dist/react/RichTextRenderedBlock.d.ts +0 -4
  104. package/dist/react/RichTextRenderedBlock.js +0 -36
  105. package/dist/react/RichTextRenderer.d.ts +0 -4
  106. package/dist/react/RichTextRenderer.js +0 -8
  107. package/dist/react/RichTextStyles.js +0 -719
  108. package/dist/react/SpecialBlockOption.d.ts +0 -7
  109. package/dist/react/SpecialBlockOption.js +0 -7
  110. package/dist/react/SpecialBlockTool.d.ts +0 -42
  111. package/dist/react/SpecialBlockTool.js +0 -50
  112. package/dist/react/blockActionToolState.d.ts +0 -18
  113. package/dist/react/blockActionToolState.js +0 -53
  114. package/dist/react/blockActions.d.ts +0 -8
  115. package/dist/react/blockActions.js +0 -111
  116. package/dist/richText.js +0 -297
  117. package/dist/types.d.ts +0 -34
  118. package/dist-cjs/react/RichTextBody.js +0 -69
  119. package/dist-cjs/react/RichTextEditor.js +0 -1108
  120. package/dist-cjs/react/RichTextRenderedBlock.js +0 -39
  121. package/dist-cjs/react/RichTextRenderer.js +0 -11
  122. package/dist-cjs/react/RichTextStyles.js +0 -722
  123. package/dist-cjs/react/SpecialBlockOption.js +0 -10
  124. package/dist-cjs/react/SpecialBlockTool.js +0 -54
  125. package/dist-cjs/react/blockActionToolState.js +0 -58
  126. package/dist-cjs/react/blockActions.js +0 -119
  127. package/dist-cjs/richText.js +0 -307
  128. /package/dist/{types.js → core/types.js} +0 -0
  129. /package/dist/{writingStats.d.ts → core/writingStats.d.ts} +0 -0
  130. /package/dist/{writingStats.js → core/writingStats.js} +0 -0
  131. /package/dist/react/{RichTextDocumentSurface.d.ts → editor/RichTextDocumentSurface.d.ts} +0 -0
  132. /package/dist/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  133. /package/dist/react/{UnsavedChangesDialog.d.ts → session/UnsavedChangesDialog.d.ts} +0 -0
  134. /package/dist/react/{RichTextStyles.d.ts → styles/RichTextStyles.d.ts} +0 -0
  135. /package/dist/react/{richTextBlockStyles.d.ts → styles/richTextBlockStyles.d.ts} +0 -0
  136. /package/dist/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  137. /package/dist/react/{specialBlockStyles.d.ts → styles/specialBlockStyles.d.ts} +0 -0
  138. /package/dist/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  139. /package/dist/{saveControl.d.ts → session/saveControl.d.ts} +0 -0
  140. /package/dist/{saveControl.js → session/saveControl.js} +0 -0
  141. /package/dist/{session.js → session/session.js} +0 -0
  142. /package/dist/{sessionRegistry.d.ts → session/sessionRegistry.d.ts} +0 -0
  143. /package/dist/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
  144. /package/dist-cjs/{types.js → core/types.js} +0 -0
  145. /package/dist-cjs/{writingStats.js → core/writingStats.js} +0 -0
  146. /package/dist-cjs/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  147. /package/dist-cjs/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  148. /package/dist-cjs/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  149. /package/dist-cjs/{saveControl.js → session/saveControl.js} +0 -0
  150. /package/dist-cjs/{session.js → session/session.js} +0 -0
  151. /package/dist-cjs/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
@@ -0,0 +1,2925 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { getTextBlockShortcut, } from "./editorShortcuts.js";
6
+ import { BlockActionTool } from "../tools/BlockActionTool.js";
7
+ import { RichTextBody } from "./RichTextBody.js";
8
+ import { LinkCreationInput } from "../tools/LinkCreationInput.js";
9
+ import { RichTextDocumentSurface, } from "./RichTextDocumentSurface.js";
10
+ import { RichTextTitleInput } from "./RichTextTitleInput.js";
11
+ import { SelectionFormatToolbar, } from "../tools/SelectionFormatToolbar.js";
12
+ import { blockActionToClipboardText, blockToClipboardText, convertTreeRowBlockToParagraph, deleteBlockById, getBlockActionBlocks, getBlockDeletionFocusTarget, getForwardDeleteFocusTarget, getToggleContentBackspaceDeletionFocusTarget, indentBlock, insertBlocksAfterBlock, insertBlocksInsideBlock, isFirstToggleContentBlock, isRootBlockAfterToggle, isBlockActionTarget, outdentBlock, reorderBlock, replaceBlockWithBlocks, setToggleCollapsed, splitTextBlock, splitTreeRowBlock, } from "../editor/blockActions.js";
13
+ import { getBlockActionToolAnchorTop, getPointerDragDropResult, getPointerHoverLaneBlockId, getUniqueBlockActionToolPlacements, getVisibleBlockActionToolPlacements, } from "../tools/blockActionToolState.js";
14
+ import { createRichTextBlockId, decodeRichTextBlocksFromClipboardText, editorHtmlToMarkdown, encodeRichTextBlocksForClipboard, isRichTextBlocksEmpty, markdownToEditorHtml, sanitizeRichTextBlocks, } from "../../core/richText.js";
15
+ import { canonicalSerialize } from "../../session/session.js";
16
+ import { isNestableRichTextBlock } from "../../core/blockTypes.js";
17
+ import { findBlockPath, getBlockAtPath, updateBlockAtPath, } from "../../core/blockTree.js";
18
+ import { getRichTextFeatureAccess, isRichTextFeatureEnabled, } from "../../core/features.js";
19
+ import { getWritingStats } from "../../core/writingStats.js";
20
+ import { SpecialBlockTool, } from "../tools/SpecialBlockTool.js";
21
+ import { RichTextStyleScope } from "../styles/RichTextStyles.js";
22
+ import { TranscriptionControl } from "../tools/TranscriptionControl.js";
23
+ import { canRedoRichTextHistory, canUndoRichTextHistory, commitRichTextHistory, createRichTextHistorySnapshot, createRichTextHistory, redoRichTextHistory, undoRichTextHistory, } from "./editorHistory.js";
24
+ import { captureRichTextSelection, restoreRichTextSelection, } from "./editorSelection.js";
25
+ const SELECTION_TOOLBAR_HALF_WIDTH = 92;
26
+ const SELECTION_TOOLBAR_HEIGHT = 48;
27
+ const SELECTION_TOOLBAR_SELECTION_GAP = 48;
28
+ export const defaultRichTextEditorBehavior = {
29
+ blockActions: true,
30
+ specialBlocks: true,
31
+ textBlockShortcuts: true,
32
+ };
33
+ export function resolveRichTextEditorBehavior(behavior) {
34
+ return {
35
+ ...defaultRichTextEditorBehavior,
36
+ ...behavior,
37
+ };
38
+ }
39
+ const IMAGE_CUSTOM_WIDTH_FALLBACK = 600;
40
+ // Coordinates the title field, editable body, format toolbar, and block insertion controls.
41
+ export function RichTextEditor({ behavior, bodyLabel, contentBlocks, disabled = false, documentBackground = "transparent", features, helperText, imageUpload, lockedFeatureMode, onFeatureGate, onContentBlocksChange, onHistoryControlsChange, onHistorySnapshotChange, onTitleChange, required = false, title, titleLabel, titleValidationMessage = "", transcriptionEnabled, transcriptionLanguage, transcriptionPortalContainer, }) {
42
+ const bodyRef = useRef(null);
43
+ const blockPointerDragRef = useRef(null);
44
+ const blockPointerDragCleanupRef = useRef(null);
45
+ const imageInputRef = useRef(null);
46
+ const specialBlockTargetRef = useRef(null);
47
+ const titleRef = useRef(null);
48
+ const [activeBlockActionMenuId, setActiveBlockActionMenuId] = useState(null);
49
+ const [blockActionToolHover, setBlockActionToolHover] = useState(false);
50
+ const [blockActionToolPlacements, setBlockActionToolPlacements] = useState([]);
51
+ const [blockDragDropResult, setBlockDragDropResult] = useState(null);
52
+ const [draggedBlockId, setDraggedBlockId] = useState(null);
53
+ const [focusedBlockId, setFocusedBlockId] = useState(null);
54
+ const [hoveredBlockId, setHoveredBlockId] = useState(null);
55
+ const [bodyActive, setBodyActive] = useState(false);
56
+ const [linkInputOpen, setLinkInputOpen] = useState(false);
57
+ const pendingLinkRangeRef = useRef(null);
58
+ const [specialBlockToolHover, setSpecialBlockToolHover] = useState(false);
59
+ const [specialBlockToolOpen, setSpecialBlockToolOpen] = useState(false);
60
+ const [specialBlockToolTop, setSpecialBlockToolTop] = useState(0);
61
+ const [selectionActive, setSelectionActive] = useState(false);
62
+ const [selectionToolbarPosition, setSelectionToolbarPosition] = useState({
63
+ left: 0,
64
+ placement: "above",
65
+ top: 0,
66
+ });
67
+ const editorBehavior = useMemo(() => resolveRichTextEditorBehavior(behavior), [behavior]);
68
+ const blockActionsEnabled = editorBehavior.blockActions;
69
+ const specialBlocksEnabled = editorBehavior.specialBlocks;
70
+ const textBlockShortcutsEnabled = editorBehavior.textBlockShortcuts;
71
+ const [specialBlockToolVisible, setSpecialBlockToolVisible] = useState(false);
72
+ const sanitizedContentBlocks = useMemo(() => sanitizeRichTextBlocks(contentBlocks), [contentBlocks]);
73
+ const currentDocument = useMemo(() => ({
74
+ contentBlocks: sanitizedContentBlocks,
75
+ title,
76
+ }), [sanitizedContentBlocks, title]);
77
+ const [history, setHistory] = useState(() => createRichTextHistory({
78
+ document: currentDocument,
79
+ selection: null,
80
+ }));
81
+ const historyRef = useRef(history);
82
+ const applyHistoryEntryRef = useRef(() => undefined);
83
+ const editableHtml = useMemo(() => richTextBlocksToEditorHtml(sanitizedContentBlocks), [sanitizedContentBlocks]);
84
+ const stats = useMemo(() => getWritingStats(sanitizedContentBlocks), [sanitizedContentBlocks]);
85
+ const transcriptionConfig = resolveTranscriptionConfig({
86
+ enabled: (transcriptionEnabled ?? true) &&
87
+ isRichTextFeatureEnabled(features, "transcription"),
88
+ language: transcriptionLanguage,
89
+ });
90
+ const transcriptionControl = transcriptionConfig.enabled && !disabled ? (_jsx(TranscriptionControl, { language: transcriptionConfig.language, onTranscript: insertTranscriptText })) : null;
91
+ const transcriptionNode = transcriptionPortalContainer === null
92
+ ? null
93
+ : transcriptionPortalContainer && transcriptionControl
94
+ ? createPortal(transcriptionControl, transcriptionPortalContainer)
95
+ : transcriptionControl;
96
+ useEffect(() => {
97
+ historyRef.current = history;
98
+ }, [history]);
99
+ useEffect(() => {
100
+ onHistorySnapshotChange?.(createRichTextHistorySnapshot(history));
101
+ }, [history, onHistorySnapshotChange]);
102
+ useEffect(() => {
103
+ const body = bodyRef.current;
104
+ const editorFocused = (body &&
105
+ (document.activeElement === body ||
106
+ body.contains(document.activeElement))) ||
107
+ document.activeElement === titleRef.current;
108
+ if (editorFocused ||
109
+ canonicalSerialize(historyRef.current.present.document) ===
110
+ canonicalSerialize(currentDocument)) {
111
+ return;
112
+ }
113
+ const nextHistory = createRichTextHistory({
114
+ document: currentDocument,
115
+ selection: null,
116
+ });
117
+ historyRef.current = nextHistory;
118
+ setHistory(nextHistory);
119
+ }, [currentDocument]);
120
+ const syncBlockActionToolPlacements = useCallback((nextPlacements) => {
121
+ setBlockActionToolPlacements((currentPlacements) => {
122
+ return areBlockActionToolPlacementsEqual(currentPlacements, nextPlacements)
123
+ ? currentPlacements
124
+ : nextPlacements;
125
+ });
126
+ }, []);
127
+ const updateBlockActionTools = useCallback((body = bodyRef.current) => {
128
+ if (!blockActionsEnabled || !body) {
129
+ setBlockActionToolPlacements([]);
130
+ return;
131
+ }
132
+ const nextPlacements = getBlockActionToolPlacements(body);
133
+ syncBlockActionToolPlacements(nextPlacements);
134
+ }, [blockActionsEnabled, syncBlockActionToolPlacements]);
135
+ const refreshBlockActionTools = useCallback((body = bodyRef.current) => {
136
+ updateBlockActionTools(body);
137
+ window.requestAnimationFrame(() => {
138
+ updateBlockActionTools(body);
139
+ });
140
+ }, [updateBlockActionTools]);
141
+ useEffect(() => {
142
+ const body = bodyRef.current;
143
+ if (body &&
144
+ shouldRefreshEditorBodyHtml(body, editableHtml, sanitizedContentBlocks) &&
145
+ (!bodyActive || isEditorHtmlEmpty(editableHtml))) {
146
+ body.innerHTML = editableHtml;
147
+ updateBlockActionTools(body);
148
+ }
149
+ }, [
150
+ bodyActive,
151
+ editableHtml,
152
+ sanitizedContentBlocks,
153
+ updateBlockActionTools,
154
+ ]);
155
+ useEffect(() => {
156
+ const handleResize = () => updateBlockActionTools();
157
+ window.addEventListener("resize", handleResize);
158
+ return () => window.removeEventListener("resize", handleResize);
159
+ }, [updateBlockActionTools]);
160
+ useEffect(() => {
161
+ return () => {
162
+ blockPointerDragCleanupRef.current?.();
163
+ };
164
+ }, []);
165
+ function setSpecialBlockTarget(block, body = bodyRef.current) {
166
+ specialBlockTargetRef.current = block;
167
+ setSpecialBlockToolVisible(!!block);
168
+ if (block && body) {
169
+ setSpecialBlockToolTop(getBlockInsertTop(block, body) ?? 0);
170
+ }
171
+ }
172
+ function syncSelectionState() {
173
+ const body = bodyRef.current;
174
+ const selection = window.getSelection();
175
+ if (!body) {
176
+ setSelectionActive(false);
177
+ setBlockActionToolPlacements([]);
178
+ return;
179
+ }
180
+ updateBlockActionTools(body);
181
+ if (!selection || selection.rangeCount === 0) {
182
+ setSelectionActive(false);
183
+ setSpecialBlockTarget(null);
184
+ setFocusedBlockId(null);
185
+ return;
186
+ }
187
+ const range = selection.getRangeAt(0);
188
+ if (!body.contains(range.commonAncestorContainer)) {
189
+ setSelectionActive(false);
190
+ setSpecialBlockTarget(null);
191
+ setFocusedBlockId(null);
192
+ return;
193
+ }
194
+ const bodyFocused = document.activeElement === body || body.contains(document.activeElement);
195
+ const rect = getVisibleRangeRect(range);
196
+ const bodyRect = body.getBoundingClientRect();
197
+ const activeBlock = getActiveEditorBlock(range, body);
198
+ setFocusedBlockId(bodyFocused && activeBlock ? ensureEditorBlockId(activeBlock) : null);
199
+ if (!selection.isCollapsed && rect.width && rect.height) {
200
+ setSelectionActive(true);
201
+ setSpecialBlockTarget(null);
202
+ setSelectionToolbarPosition(getSelectionToolbarPosition({
203
+ bodyClientWidth: body.clientWidth,
204
+ bodyRect,
205
+ rangeRect: rect,
206
+ scrollTop: body.scrollTop,
207
+ }));
208
+ return;
209
+ }
210
+ setSelectionActive(false);
211
+ }
212
+ function getCurrentEditorDocument() {
213
+ const body = bodyRef.current;
214
+ return {
215
+ contentBlocks: body
216
+ ? readBlocksFromEditor(body)
217
+ : sanitizeRichTextBlocks(contentBlocks),
218
+ title,
219
+ };
220
+ }
221
+ function commitEditorDocumentChange({ change, focusBlockId, focusBlockPosition, focusBody, historyLabel = "typing", historyMetadata, nextContentBlocks, nextTitle = title, selection, skipBodyHtmlUpdate = false, }) {
222
+ const body = bodyRef.current;
223
+ const sanitizedBlocks = sanitizeRichTextBlocks(nextContentBlocks ?? getCurrentEditorDocument().contentBlocks);
224
+ const nextDocument = {
225
+ contentBlocks: sanitizedBlocks,
226
+ title: nextTitle,
227
+ };
228
+ const nextSelection = selection === undefined ? captureRichTextSelection(body) : selection;
229
+ setHistory((currentHistory) => {
230
+ const nextHistory = commitRichTextHistory(currentHistory, {
231
+ document: nextDocument,
232
+ label: historyLabel,
233
+ metadata: historyMetadata,
234
+ selection: nextSelection,
235
+ });
236
+ historyRef.current = nextHistory;
237
+ return nextHistory;
238
+ });
239
+ if (!body) {
240
+ if (nextTitle !== title) {
241
+ onTitleChange(nextTitle);
242
+ }
243
+ onContentBlocksChange(sanitizedBlocks, change);
244
+ setSpecialBlockToolOpen(false);
245
+ return;
246
+ }
247
+ if (!skipBodyHtmlUpdate) {
248
+ body.innerHTML = richTextBlocksToEditorHtml(sanitizedBlocks);
249
+ }
250
+ if (nextTitle !== title) {
251
+ onTitleChange(nextTitle);
252
+ }
253
+ onContentBlocksChange(sanitizedBlocks, change);
254
+ updateBlockActionTools(body);
255
+ if (focusBody ?? true) {
256
+ body.focus();
257
+ }
258
+ if (focusBlockId) {
259
+ focusEditorBlock(body, focusBlockId, focusBlockPosition);
260
+ }
261
+ else if (nextSelection) {
262
+ restoreRichTextSelection(body, nextSelection);
263
+ }
264
+ setSpecialBlockToolOpen(false);
265
+ syncSelectionState();
266
+ }
267
+ function applyHistoryEntry(entry) {
268
+ const body = bodyRef.current;
269
+ if (entry.document.title !== title) {
270
+ onTitleChange(entry.document.title);
271
+ }
272
+ onContentBlocksChange(entry.document.contentBlocks);
273
+ if (!body) {
274
+ return;
275
+ }
276
+ restoreScrollPositionAfter(window, () => {
277
+ body.innerHTML = richTextBlocksToEditorHtml(entry.document.contentBlocks);
278
+ });
279
+ restoreRichTextSelection(body, entry.selection);
280
+ refreshBlockActionTools(body);
281
+ syncSelectionState();
282
+ }
283
+ useLayoutEffect(() => {
284
+ applyHistoryEntryRef.current = applyHistoryEntry;
285
+ });
286
+ const handleUndo = useCallback(() => {
287
+ const result = undoRichTextHistory(historyRef.current);
288
+ if (result.history === historyRef.current) {
289
+ return false;
290
+ }
291
+ historyRef.current = result.history;
292
+ setHistory(result.history);
293
+ applyHistoryEntryRef.current(result.entry);
294
+ return true;
295
+ }, []);
296
+ const handleRedo = useCallback(() => {
297
+ const result = redoRichTextHistory(historyRef.current);
298
+ if (result.history === historyRef.current) {
299
+ return false;
300
+ }
301
+ historyRef.current = result.history;
302
+ setHistory(result.history);
303
+ applyHistoryEntryRef.current(result.entry);
304
+ return true;
305
+ }, []);
306
+ useEffect(() => {
307
+ onHistoryControlsChange?.({
308
+ canRedo: canRedoRichTextHistory(history),
309
+ canUndo: canUndoRichTextHistory(history),
310
+ redo: handleRedo,
311
+ undo: handleUndo,
312
+ });
313
+ }, [handleRedo, handleUndo, history, onHistoryControlsChange]);
314
+ function emitBodyChange(historyLabel = "typing", historyMetadata) {
315
+ const body = bodyRef.current;
316
+ if (!body) {
317
+ return;
318
+ }
319
+ commitEditorDocumentChange({
320
+ historyLabel,
321
+ historyMetadata,
322
+ nextContentBlocks: readBlocksFromEditor(body),
323
+ skipBodyHtmlUpdate: true,
324
+ });
325
+ refreshBlockActionTools(body);
326
+ }
327
+ function handleTitleChange(nextTitle) {
328
+ const nextDocument = {
329
+ contentBlocks: getCurrentEditorDocument().contentBlocks,
330
+ title: nextTitle,
331
+ };
332
+ setHistory((currentHistory) => {
333
+ const nextHistory = commitRichTextHistory(currentHistory, {
334
+ document: nextDocument,
335
+ label: "title",
336
+ selection: captureRichTextSelection(bodyRef.current),
337
+ });
338
+ historyRef.current = nextHistory;
339
+ return nextHistory;
340
+ });
341
+ onTitleChange(nextTitle);
342
+ }
343
+ function handleBodyInput() {
344
+ const appliedShortcut = textBlockShortcutsEnabled
345
+ ? applyActiveTextBlockShortcut(bodyRef.current)
346
+ : false;
347
+ if (!appliedShortcut) {
348
+ normalizeActiveTextBlockTypography(bodyRef.current);
349
+ }
350
+ emitBodyChange(appliedShortcut ? "block-insert" : "typing", appliedShortcut ? { blockType: appliedShortcut } : undefined);
351
+ syncSelectionState();
352
+ }
353
+ function handleBodyEnter(event) {
354
+ const body = bodyRef.current;
355
+ const selection = window.getSelection();
356
+ if (!body || !selection || selection.rangeCount === 0) {
357
+ return false;
358
+ }
359
+ const range = selection.getRangeAt(0);
360
+ if (!body.contains(range.commonAncestorContainer)) {
361
+ return false;
362
+ }
363
+ const activeBlock = getActiveEditorBlock(range, body);
364
+ if (!activeBlock) {
365
+ return false;
366
+ }
367
+ const treeRow = getTreeRowEditorParts(activeBlock);
368
+ if (treeRow) {
369
+ if (!treeRow.label.contains(range.startContainer)) {
370
+ return false;
371
+ }
372
+ if (!selection.isCollapsed) {
373
+ range.deleteContents();
374
+ }
375
+ if (getCheckboxEnterAction(event) === "line-break") {
376
+ insertLineBreakAtRange(range, {
377
+ padTrailingBreak: shouldPadTrailingLineBreak(getTextAfterRange(treeRow.label, range)),
378
+ });
379
+ emitBodyChange();
380
+ syncSelectionState();
381
+ return true;
382
+ }
383
+ const splitOffset = getTextOffsetWithin(treeRow.label, range);
384
+ const splitMarkdown = splitTreeRowMarkdownAtTextOffset(editorHtmlToMarkdown(normalizeSoftBreakHtml(treeRow.label.innerHTML)), splitOffset);
385
+ const blockId = activeBlock.getAttribute("data-block-id");
386
+ const nextBlockId = createRichTextBlockId();
387
+ if (!blockId) {
388
+ return false;
389
+ }
390
+ if ((treeRow.label.textContent ?? "").trim().length === 0) {
391
+ applyContentBlocksChange(convertTreeRowBlockToParagraph(readBlocksFromEditor(body), blockId, ""), {
392
+ focusBlockId: blockId,
393
+ focusBlockPosition: "start",
394
+ historyLabel: "block-outdent",
395
+ });
396
+ return true;
397
+ }
398
+ applyContentBlocksChange(splitTreeRowBlock(readBlocksFromEditor(body), blockId, {
399
+ afterMarkdown: splitMarkdown.after,
400
+ beforeMarkdown: splitMarkdown.before,
401
+ nextBlockId,
402
+ }), {
403
+ focusBlockId: nextBlockId,
404
+ historyLabel: "block-insert",
405
+ historyMetadata: {
406
+ blockType: getHistoryBlockTypeForElement(activeBlock),
407
+ },
408
+ });
409
+ return true;
410
+ }
411
+ if (!event.shiftKey && isSoftBreakTextBlock(activeBlock)) {
412
+ if (!selection.isCollapsed) {
413
+ range.deleteContents();
414
+ }
415
+ const blockId = ensureEditorBlockId(activeBlock);
416
+ const splitOffset = getTextOffsetWithin(activeBlock, range);
417
+ const splitMarkdown = splitTreeRowMarkdownAtTextOffset(editorHtmlToMarkdown(normalizeSoftBreakHtml(activeBlock.innerHTML)), splitOffset);
418
+ const nextBlockId = createRichTextBlockId();
419
+ applyContentBlocksChange(splitTextBlock(readBlocksFromEditor(body), blockId, {
420
+ afterMarkdown: splitMarkdown.after,
421
+ beforeMarkdown: splitMarkdown.before,
422
+ nextBlockId,
423
+ }), {
424
+ focusBlockId: nextBlockId,
425
+ focusBlockPosition: "start",
426
+ historyLabel: "block-insert",
427
+ historyMetadata: {
428
+ blockType: getHistoryBlockTypeForElement(activeBlock),
429
+ },
430
+ });
431
+ return true;
432
+ }
433
+ if (event.shiftKey && isSoftBreakTextBlock(activeBlock)) {
434
+ if (!selection.isCollapsed) {
435
+ range.deleteContents();
436
+ }
437
+ insertLineBreakAtRange(range, {
438
+ padTrailingBreak: shouldPadTrailingLineBreak(getTextAfterRange(activeBlock, range)),
439
+ });
440
+ emitBodyChange();
441
+ syncSelectionState();
442
+ return true;
443
+ }
444
+ return false;
445
+ }
446
+ function handleBodyBackspace() {
447
+ const body = bodyRef.current;
448
+ const selection = window.getSelection();
449
+ if (!body || !selection || selection.rangeCount === 0) {
450
+ return false;
451
+ }
452
+ const range = selection.getRangeAt(0);
453
+ if (!selection.isCollapsed ||
454
+ !body.contains(range.commonAncestorContainer)) {
455
+ return false;
456
+ }
457
+ const activeBlock = getActiveEditorBlock(range, body);
458
+ const activeBlockId = activeBlock?.getAttribute("data-block-id") ?? "";
459
+ const activeBlockAtStart = activeBlock !== null && isEditorBlockAtBackspaceStart(activeBlock, range);
460
+ if (activeBlockId && activeBlockAtStart) {
461
+ const currentBlocks = readBlocksFromEditor(body);
462
+ if (isFirstToggleContentBlock(currentBlocks, activeBlockId) ||
463
+ isRootBlockAfterToggle(currentBlocks, activeBlockId)) {
464
+ return true;
465
+ }
466
+ }
467
+ if (activeBlock &&
468
+ isDirectToggleContentChild(activeBlock) &&
469
+ isEmptyTextBlockAtStart(activeBlock, range)) {
470
+ const blockId = activeBlock.getAttribute("data-block-id");
471
+ if (!blockId) {
472
+ return true;
473
+ }
474
+ const currentBlocks = readBlocksFromEditor(body);
475
+ const deletionFocusTarget = getToggleContentBackspaceDeletionFocusTarget(currentBlocks, blockId);
476
+ if (!deletionFocusTarget) {
477
+ return true;
478
+ }
479
+ applyContentBlocksChange(deleteBlockById(currentBlocks, blockId), {
480
+ focusBlockId: deletionFocusTarget.blockId,
481
+ focusBlockPosition: deletionFocusTarget.position,
482
+ historyLabel: "block-delete",
483
+ });
484
+ return true;
485
+ }
486
+ if (activeBlock &&
487
+ !isDirectToggleContentChild(activeBlock) &&
488
+ handleEmptyTextBlockBackspace(activeBlock, range, body)) {
489
+ return true;
490
+ }
491
+ const treeRow = getTreeRowEditorParts(activeBlock);
492
+ if (!activeBlock || !treeRow) {
493
+ return false;
494
+ }
495
+ if (!treeRow.label.contains(range.startContainer)) {
496
+ return false;
497
+ }
498
+ if (getTextOffsetWithin(treeRow.label, range) !== 0) {
499
+ return false;
500
+ }
501
+ const blockId = activeBlock.getAttribute("data-block-id");
502
+ if (!blockId) {
503
+ return false;
504
+ }
505
+ const currentBlocks = readBlocksFromEditor(body);
506
+ const currentBlock = getBlockById(currentBlocks, blockId);
507
+ const currentTreeRowMarkdown = isNestableRichTextBlock(currentBlock)
508
+ ? currentBlock.markdown
509
+ : editorHtmlToMarkdown(normalizeSoftBreakHtml(treeRow.label.innerHTML));
510
+ const currentTreeRowIsEmpty = currentTreeRowMarkdown.trim().length === 0;
511
+ if (treeRow.type === "toggle") {
512
+ if (!currentTreeRowIsEmpty) {
513
+ return false;
514
+ }
515
+ if (!canBackspaceEmptyToggleTitle(currentBlock)) {
516
+ return true;
517
+ }
518
+ }
519
+ const paragraphMarkdown = getTreeRowBackspaceParagraphMarkdown(treeRow.type, currentTreeRowMarkdown);
520
+ applyContentBlocksChange(convertTreeRowBlockToParagraph(currentBlocks, blockId, paragraphMarkdown, {
521
+ convertNonEmpty: treeRow.type !== "toggle",
522
+ }), {
523
+ focusBlockId: blockId,
524
+ focusBlockPosition: getTreeRowBackspaceFocusPositionForParagraph(treeRow.type, currentTreeRowMarkdown),
525
+ historyLabel: "block-delete",
526
+ });
527
+ return true;
528
+ }
529
+ function handleEmptyTextBlockBackspace(activeBlock, range, body) {
530
+ if (!isEmptyTextBlockAtStart(activeBlock, range)) {
531
+ return false;
532
+ }
533
+ const blockId = activeBlock.getAttribute("data-block-id");
534
+ if (!blockId) {
535
+ return false;
536
+ }
537
+ const currentBlocks = readBlocksFromEditor(body);
538
+ const deletionFocusTarget = getBlockDeletionFocusTarget(currentBlocks, blockId);
539
+ applyContentBlocksChange(deleteBlockById(currentBlocks, blockId), {
540
+ focusBlockId: deletionFocusTarget?.blockId,
541
+ focusBlockPosition: deletionFocusTarget?.position,
542
+ historyLabel: "block-delete",
543
+ });
544
+ return true;
545
+ }
546
+ function handleBodyDelete() {
547
+ const body = bodyRef.current;
548
+ const selection = window.getSelection();
549
+ if (!body || !selection || selection.rangeCount === 0) {
550
+ return false;
551
+ }
552
+ const range = selection.getRangeAt(0);
553
+ if (!selection.isCollapsed ||
554
+ !body.contains(range.commonAncestorContainer)) {
555
+ return false;
556
+ }
557
+ const activeBlock = getActiveEditorBlock(range, body);
558
+ const blockId = activeBlock?.getAttribute("data-block-id");
559
+ if (!activeBlock || !blockId) {
560
+ return false;
561
+ }
562
+ if (!isCaretInDeletableBlockContent(activeBlock, range)) {
563
+ return false;
564
+ }
565
+ const currentBlocks = readBlocksFromEditor(body);
566
+ const currentBlock = getBlockById(currentBlocks, blockId);
567
+ if (!isEmptyEditableBlockWithoutChildren(currentBlock)) {
568
+ return false;
569
+ }
570
+ const deletionFocusTarget = getForwardDeleteFocusTarget(currentBlocks, blockId);
571
+ if (!deletionFocusTarget) {
572
+ return true;
573
+ }
574
+ applyContentBlocksChange(deleteBlockById(currentBlocks, blockId), {
575
+ focusBlockId: deletionFocusTarget.blockId,
576
+ focusBlockPosition: deletionFocusTarget.position,
577
+ historyLabel: "block-delete",
578
+ });
579
+ return true;
580
+ }
581
+ function handleBodyTab(event) {
582
+ const body = bodyRef.current;
583
+ const selection = window.getSelection();
584
+ if (!body || !selection || selection.rangeCount === 0) {
585
+ return false;
586
+ }
587
+ const activeBlock = getActiveEditorBlock(selection.getRangeAt(0), body);
588
+ const activeBlockId = activeBlock?.getAttribute("data-block-id") ?? focusedBlockId;
589
+ const activeTreeRow = getTreeRowEditorParts(activeBlock);
590
+ const activeTreeRowOffset = activeTreeRow?.label.contains(selection.getRangeAt(0).startContainer)
591
+ ? getTextOffsetWithin(activeTreeRow.label, selection.getRangeAt(0))
592
+ : null;
593
+ if (!activeBlockId) {
594
+ return false;
595
+ }
596
+ const currentBlocks = readBlocksFromEditor(body);
597
+ const activeRichTextBlock = getBlockById(currentBlocks, activeBlockId);
598
+ if (!isNestableEditorBlock(activeBlock) &&
599
+ !isNestableRichTextBlock(activeRichTextBlock)) {
600
+ return true;
601
+ }
602
+ const nextBlocks = event.shiftKey
603
+ ? outdentBlock(currentBlocks, activeBlockId)
604
+ : indentBlock(currentBlocks, activeBlockId);
605
+ if (JSON.stringify(nextBlocks) === JSON.stringify(currentBlocks)) {
606
+ return true;
607
+ }
608
+ applyContentBlocksChange(nextBlocks, {
609
+ focusBody: false,
610
+ historyLabel: event.shiftKey ? "block-outdent" : "block-indent",
611
+ });
612
+ focusTreeRowLabelAtTextOffset(body.querySelector(`[data-block-id="${cssEscape(activeBlockId)}"]`), activeTreeRowOffset);
613
+ syncSelectionState();
614
+ return true;
615
+ }
616
+ function handleBodyPasteText(text, pasteTargetBlock) {
617
+ if (!specialBlocksEnabled) {
618
+ return false;
619
+ }
620
+ const pastedBlocks = decodeRichTextBlocksFromClipboardText(text);
621
+ if (!pastedBlocks) {
622
+ return false;
623
+ }
624
+ const body = bodyRef.current;
625
+ const selection = window.getSelection();
626
+ if (!body) {
627
+ onContentBlocksChange(pastedBlocks);
628
+ return true;
629
+ }
630
+ const selectedActiveBlock = selection && selection.rangeCount > 0
631
+ ? getActiveEditorBlock(selection.getRangeAt(0), body)
632
+ : null;
633
+ const activeBlock = pasteTargetBlock && body.contains(pasteTargetBlock)
634
+ ? pasteTargetBlock
635
+ : selectedActiveBlock;
636
+ const activeBlockId = activeBlock?.getAttribute("data-block-id");
637
+ const currentBlocks = readBlocksFromEditor(body);
638
+ const toggleContentPlaceholderParentId = getToggleContentPlaceholderParentId(activeBlock);
639
+ const currentBlock = activeBlockId
640
+ ? getBlockById(currentBlocks, activeBlockId)
641
+ : null;
642
+ const nextBlocks = toggleContentPlaceholderParentId
643
+ ? insertBlocksInsideBlock(currentBlocks, toggleContentPlaceholderParentId, pastedBlocks)
644
+ : activeBlockId && isEmptyEditableBlockWithoutChildren(currentBlock)
645
+ ? replaceBlockWithBlocks(currentBlocks, activeBlockId, pastedBlocks)
646
+ : activeBlockId
647
+ ? insertBlocksAfterBlock(currentBlocks, activeBlockId, pastedBlocks)
648
+ : [...currentBlocks, ...pastedBlocks];
649
+ const focusBlock = pastedBlocks[0];
650
+ applyContentBlocksChange(nextBlocks, {
651
+ focusBlockId: focusBlock?.id,
652
+ focusBlockPosition: "start",
653
+ historyLabel: "paste",
654
+ });
655
+ return true;
656
+ }
657
+ function handleToggleCollapse(blockId) {
658
+ const body = bodyRef.current;
659
+ if (!body) {
660
+ return;
661
+ }
662
+ const currentBlocks = readBlocksFromEditor(body);
663
+ const currentBlock = getBlockById(currentBlocks, blockId);
664
+ if (currentBlock?.type !== "toggle") {
665
+ return;
666
+ }
667
+ restoreScrollPositionAfter(window, () => applyContentBlocksChange(setToggleCollapsed(currentBlocks, blockId, !currentBlock.collapsed), {
668
+ focusBlockId: blockId,
669
+ focusBlockPosition: "start",
670
+ historyLabel: "toggle-collapse",
671
+ }, { reason: "toggle-collapse" }));
672
+ }
673
+ function runSelectionCommand(command) {
674
+ bodyRef.current?.focus();
675
+ if (command === "link") {
676
+ openLinkInput();
677
+ return;
678
+ }
679
+ else if (command === "heading") {
680
+ closeLinkInput();
681
+ document.execCommand("formatBlock", false, "h2");
682
+ }
683
+ else if (command === "quote") {
684
+ closeLinkInput();
685
+ document.execCommand("formatBlock", false, "blockquote");
686
+ }
687
+ else if (command === "code") {
688
+ closeLinkInput();
689
+ wrapSelectionWith("code");
690
+ }
691
+ else {
692
+ closeLinkInput();
693
+ document.execCommand(command, false);
694
+ }
695
+ emitBodyChange("format");
696
+ syncSelectionState();
697
+ }
698
+ function openLinkInput() {
699
+ const body = bodyRef.current;
700
+ const selection = window.getSelection();
701
+ if (!body || !selection || selection.rangeCount === 0) {
702
+ return;
703
+ }
704
+ const range = selection.getRangeAt(0);
705
+ if (range.collapsed || !body.contains(range.commonAncestorContainer)) {
706
+ return;
707
+ }
708
+ pendingLinkRangeRef.current = range.cloneRange();
709
+ setSelectionActive(false);
710
+ setLinkInputOpen(true);
711
+ }
712
+ function closeLinkInput() {
713
+ pendingLinkRangeRef.current = null;
714
+ setLinkInputOpen(false);
715
+ }
716
+ function submitLinkInput(href) {
717
+ const body = bodyRef.current;
718
+ const range = pendingLinkRangeRef.current;
719
+ const selection = window.getSelection();
720
+ closeLinkInput();
721
+ if (!body || !range || !selection) {
722
+ return;
723
+ }
724
+ body.focus();
725
+ selection.removeAllRanges();
726
+ selection.addRange(range);
727
+ document.execCommand("createLink", false, href);
728
+ emitBodyChange("link");
729
+ syncSelectionState();
730
+ }
731
+ async function insertBlock(action) {
732
+ if (!specialBlocksEnabled) {
733
+ return;
734
+ }
735
+ const body = bodyRef.current;
736
+ if (!body) {
737
+ return;
738
+ }
739
+ body.focus();
740
+ if (isPasteSpecialBlockAction(action)) {
741
+ const clipboardText = await readTextFromClipboard();
742
+ if (clipboardText) {
743
+ const handledRichTextPaste = handleBodyPasteText(clipboardText, specialBlockTargetRef.current);
744
+ if (!handledRichTextPaste) {
745
+ insertPlainTextFromClipboard(clipboardText, specialBlockTargetRef.current);
746
+ }
747
+ }
748
+ setSpecialBlockToolHover(false);
749
+ syncSelectionState();
750
+ return;
751
+ }
752
+ if (isImageUploadSpecialBlockAction(action) &&
753
+ !isRichTextFeatureEnabled(features, "image-upload")) {
754
+ const access = getRichTextFeatureAccess(features, "image-upload");
755
+ if (!access.enabled) {
756
+ onFeatureGate?.({
757
+ featureId: access.featureId,
758
+ label: access.label,
759
+ reason: access.reason,
760
+ });
761
+ }
762
+ return;
763
+ }
764
+ if (isImageUploadSpecialBlockAction(action) && imageUpload) {
765
+ restoreScrollPositionAfter(window, () => imageInputRef.current?.click());
766
+ return;
767
+ }
768
+ const targetBlock = specialBlockTargetRef.current;
769
+ const html = "fallbackHtml" in action && action.kind === "image-upload"
770
+ ? action.fallbackHtml
771
+ : action.html;
772
+ const nextEditableBlockHtml = `<p data-block-id="${escapeHtmlAttribute(createRichTextBlockId())}">&nbsp;</p>`;
773
+ let insertedBlock = null;
774
+ let nextEditableBlock = null;
775
+ if (targetBlock && body.contains(targetBlock)) {
776
+ targetBlock.insertAdjacentHTML("afterend", `${html}${nextEditableBlockHtml}`);
777
+ insertedBlock = targetBlock.nextElementSibling;
778
+ nextEditableBlock = insertedBlock?.nextElementSibling ?? null;
779
+ targetBlock.remove();
780
+ }
781
+ else {
782
+ body.insertAdjacentHTML("beforeend", `${html}${nextEditableBlockHtml}`);
783
+ nextEditableBlock = body.lastElementChild;
784
+ insertedBlock = nextEditableBlock?.previousElementSibling ?? null;
785
+ }
786
+ setSpecialBlockToolHover(false);
787
+ if (action.selectDefault) {
788
+ selectBlockContents(insertedBlock);
789
+ }
790
+ else if (action.focusSelector) {
791
+ const focusTarget = insertedBlock?.querySelector(action.focusSelector) ?? insertedBlock;
792
+ action.focusPosition === "start"
793
+ ? focusBlockStart(focusTarget)
794
+ : focusBlockEnd(focusTarget);
795
+ }
796
+ else {
797
+ focusBlockStart(nextEditableBlock);
798
+ }
799
+ emitBodyChange(isImageUploadSpecialBlockAction(action) ? "image" : "block-insert", isImageUploadSpecialBlockAction(action)
800
+ ? undefined
801
+ : { blockType: getSpecialBlockActionBlockType(action) });
802
+ syncSelectionState();
803
+ }
804
+ async function handleImageFileChange(file) {
805
+ if (!specialBlocksEnabled || !file || !imageUpload) {
806
+ return;
807
+ }
808
+ const uploadBlock = insertUploadingImageBlockWithoutScrollJump(file);
809
+ try {
810
+ const upload = await imageUpload({
811
+ file,
812
+ onProgress: (progress) => updateUploadingImageBlockProgress(uploadBlock.id, progress),
813
+ });
814
+ replaceImageBlock(uploadBlock.id, buildUploadedImageBlock(upload, {
815
+ alignment: getCurrentImageAlignment(uploadBlock.id),
816
+ id: uploadBlock.id,
817
+ }));
818
+ }
819
+ catch (error) {
820
+ updateUploadingImageBlockMessage(uploadBlock.id, getImageUploadFailureMessage(error));
821
+ }
822
+ finally {
823
+ if (imageInputRef.current) {
824
+ imageInputRef.current.value = "";
825
+ }
826
+ }
827
+ }
828
+ function insertUploadingImageBlock(file) {
829
+ const body = bodyRef.current;
830
+ const imageBlock = buildUploadingImageBlock({ fileName: file.name });
831
+ const nextTextBlock = {
832
+ id: createRichTextBlockId(),
833
+ markdown: "",
834
+ type: "paragraph",
835
+ };
836
+ const insertedBlocks = [imageBlock, nextTextBlock];
837
+ if (!body) {
838
+ onContentBlocksChange(insertedBlocks);
839
+ setSpecialBlockToolOpen(false);
840
+ return imageBlock;
841
+ }
842
+ const targetBlock = specialBlockTargetRef.current;
843
+ const currentBlocks = readBlocksFromEditor(body);
844
+ const targetBlockId = targetBlock && body.contains(targetBlock)
845
+ ? targetBlock.getAttribute("data-block-id")
846
+ : null;
847
+ applyContentBlocksChange(targetBlockId
848
+ ? replaceBlockWithBlocks(currentBlocks, targetBlockId, insertedBlocks)
849
+ : [...currentBlocks, ...insertedBlocks], {
850
+ focusBlockId: nextTextBlock.id,
851
+ focusBody: false,
852
+ historyLabel: "image",
853
+ });
854
+ setSpecialBlockToolHover(false);
855
+ setSpecialBlockToolOpen(false);
856
+ return imageBlock;
857
+ }
858
+ function insertUploadingImageBlockWithoutScrollJump(file) {
859
+ const scrollX = window.scrollX;
860
+ const scrollY = window.scrollY;
861
+ const uploadBlock = insertUploadingImageBlock(file);
862
+ window.scrollTo(scrollX, scrollY);
863
+ window.requestAnimationFrame(() => window.scrollTo(scrollX, scrollY));
864
+ return uploadBlock;
865
+ }
866
+ function replaceImageBlock(blockId, imageBlock) {
867
+ const body = bodyRef.current;
868
+ if (!body) {
869
+ onContentBlocksChange(replaceBlockById(sanitizedContentBlocks, blockId, imageBlock));
870
+ return;
871
+ }
872
+ applyContentBlocksChange(replaceBlockWithBlocks(readBlocksFromEditor(body), blockId, [imageBlock]), { focusBody: false, historyLabel: "image" }, { reason: "image-upload-complete" });
873
+ }
874
+ function getCurrentImageAlignment(blockId) {
875
+ const body = bodyRef.current;
876
+ const currentBlocks = body
877
+ ? readBlocksFromEditor(body)
878
+ : sanitizedContentBlocks;
879
+ const path = findBlockPath(currentBlocks, blockId);
880
+ const block = path ? getBlockAtPath(currentBlocks, path) : null;
881
+ return block?.type === "image"
882
+ ? getImageAlignment(block.alignment)
883
+ : "center";
884
+ }
885
+ function getCurrentImageSizing(blockId) {
886
+ const body = bodyRef.current;
887
+ const currentBlocks = body
888
+ ? readBlocksFromEditor(body)
889
+ : sanitizedContentBlocks;
890
+ const path = findBlockPath(currentBlocks, blockId);
891
+ const block = path ? getBlockAtPath(currentBlocks, path) : null;
892
+ if (block?.type !== "image") {
893
+ return { displaySize: "medium" };
894
+ }
895
+ return {
896
+ displaySize: getImageDisplaySize(block.displaySize) ?? "medium",
897
+ ...(typeof block.customWidth === "number"
898
+ ? { customWidth: normalizeImageCustomWidth(block.customWidth) }
899
+ : {}),
900
+ };
901
+ }
902
+ function updateUploadingImageBlockProgress(blockId, progress) {
903
+ const nextBlock = buildUploadingImageBlock({
904
+ alignment: getCurrentImageAlignment(blockId),
905
+ id: blockId,
906
+ progress,
907
+ });
908
+ updateUploadingImageBlock(blockId, nextBlock);
909
+ }
910
+ function updateUploadingImageBlockMessage(blockId, message) {
911
+ updateUploadingImageBlock(blockId, {
912
+ alignment: getCurrentImageAlignment(blockId),
913
+ id: blockId,
914
+ type: "image",
915
+ alt: message,
916
+ });
917
+ }
918
+ function updateUploadingImageBlock(blockId, nextBlock) {
919
+ const body = bodyRef.current;
920
+ if (!body) {
921
+ onContentBlocksChange(replaceBlockById(sanitizedContentBlocks, blockId, nextBlock));
922
+ return;
923
+ }
924
+ const figure = body.querySelector(`[data-block-id="${cssEscape(blockId)}"][data-rich-text-image]`);
925
+ if (figure instanceof HTMLElement) {
926
+ const message = nextBlock.type === "image" ? (nextBlock.alt ?? "") : "";
927
+ const messageElement = figure.querySelector("p");
928
+ if (nextBlock.type === "image" &&
929
+ typeof nextBlock.uploadProgress === "number") {
930
+ figure.setAttribute("data-image-upload-progress", String(nextBlock.uploadProgress));
931
+ }
932
+ else {
933
+ figure.removeAttribute("data-image-upload-progress");
934
+ }
935
+ if (messageElement) {
936
+ messageElement.textContent = message;
937
+ }
938
+ else if (message) {
939
+ figure.textContent = "";
940
+ const paragraph = document.createElement("p");
941
+ paragraph.textContent = message;
942
+ figure.append(paragraph);
943
+ }
944
+ }
945
+ const currentBlocks = readBlocksFromEditor(body);
946
+ onContentBlocksChange(replaceBlockById(currentBlocks, blockId, nextBlock));
947
+ }
948
+ function updateImageAlignment(blockId, alignment) {
949
+ const body = bodyRef.current;
950
+ const currentBlocks = body
951
+ ? readBlocksFromEditor(body)
952
+ : sanitizedContentBlocks;
953
+ const path = findBlockPath(currentBlocks, blockId);
954
+ const currentBlock = path ? getBlockAtPath(currentBlocks, path) : null;
955
+ if (!currentBlock || currentBlock.type !== "image") {
956
+ return;
957
+ }
958
+ const nextBlocks = replaceBlockById(currentBlocks, blockId, {
959
+ ...currentBlock,
960
+ alignment,
961
+ });
962
+ const figure = body?.querySelector(`[data-block-id="${cssEscape(blockId)}"][data-rich-text-image]`);
963
+ if (!(figure instanceof HTMLElement)) {
964
+ applyContentBlocksChange(nextBlocks, {
965
+ focusBody: false,
966
+ historyLabel: "image",
967
+ });
968
+ return;
969
+ }
970
+ animateImageAlignmentChange(figure, () => {
971
+ figure.setAttribute("data-image-align", alignment);
972
+ updateImageAlignmentButtons(figure, alignment);
973
+ });
974
+ commitEditorDocumentChange({
975
+ historyLabel: "image",
976
+ nextContentBlocks: nextBlocks,
977
+ skipBodyHtmlUpdate: true,
978
+ focusBody: false,
979
+ });
980
+ updateBlockActionTools(body);
981
+ window.requestAnimationFrame(() => updateBlockActionTools(body));
982
+ }
983
+ function updateImageDisplaySize(blockId, sizing) {
984
+ const body = bodyRef.current;
985
+ const currentBlocks = body
986
+ ? readBlocksFromEditor(body)
987
+ : sanitizedContentBlocks;
988
+ const path = findBlockPath(currentBlocks, blockId);
989
+ const currentBlock = path ? getBlockAtPath(currentBlocks, path) : null;
990
+ if (!currentBlock || currentBlock.type !== "image") {
991
+ return;
992
+ }
993
+ const displaySize = getImageDisplaySize(sizing.displaySize);
994
+ const customWidth = displaySize === "custom"
995
+ ? normalizeImageCustomWidth(sizing.customWidth ?? currentBlock.customWidth)
996
+ : null;
997
+ const nextBlock = {
998
+ ...currentBlock,
999
+ ...(displaySize ? { displaySize } : {}),
1000
+ ...(customWidth ? { customWidth } : {}),
1001
+ };
1002
+ if (displaySize !== "custom") {
1003
+ delete nextBlock.customWidth;
1004
+ }
1005
+ const nextBlocks = replaceBlockById(currentBlocks, blockId, nextBlock);
1006
+ const figure = body?.querySelector(`[data-block-id="${cssEscape(blockId)}"][data-rich-text-image]`);
1007
+ if (!(figure instanceof HTMLElement)) {
1008
+ applyContentBlocksChange(nextBlocks, {
1009
+ focusBody: false,
1010
+ historyLabel: "image",
1011
+ });
1012
+ return;
1013
+ }
1014
+ updateImageDisplaySizeAttributes(figure, nextBlock);
1015
+ commitEditorDocumentChange({
1016
+ historyLabel: "image",
1017
+ nextContentBlocks: nextBlocks,
1018
+ skipBodyHtmlUpdate: true,
1019
+ focusBody: false,
1020
+ });
1021
+ updateBlockActionTools(body);
1022
+ }
1023
+ function insertPlainTextFromClipboard(text, pasteTargetBlock) {
1024
+ const body = bodyRef.current;
1025
+ if (!body || !text) {
1026
+ return false;
1027
+ }
1028
+ const targetBlock = pasteTargetBlock && body.contains(pasteTargetBlock)
1029
+ ? pasteTargetBlock
1030
+ : null;
1031
+ body.focus();
1032
+ if (targetBlock) {
1033
+ selectBlockContents(targetBlock);
1034
+ }
1035
+ document.execCommand("insertText", false, text);
1036
+ emitBodyChange("paste");
1037
+ syncSelectionState();
1038
+ return true;
1039
+ }
1040
+ function insertTranscriptText(text) {
1041
+ const body = bodyRef.current;
1042
+ const transcript = text.trim();
1043
+ if (!body || !transcript) {
1044
+ return;
1045
+ }
1046
+ body.focus();
1047
+ const selection = window.getSelection();
1048
+ if (selection &&
1049
+ selection.rangeCount > 0 &&
1050
+ body.contains(selection.getRangeAt(0).commonAncestorContainer)) {
1051
+ document.execCommand("insertText", false, appendTranscriptText("", transcript));
1052
+ }
1053
+ else {
1054
+ const currentText = body.textContent ?? "";
1055
+ body.insertAdjacentText("beforeend", appendTranscriptText(currentText, transcript).slice(currentText.length));
1056
+ focusBlockEnd(body);
1057
+ }
1058
+ emitBodyChange("transcription");
1059
+ syncSelectionState();
1060
+ }
1061
+ function focusBodyStart() {
1062
+ window.requestAnimationFrame(() => {
1063
+ const body = bodyRef.current;
1064
+ if (!body) {
1065
+ return;
1066
+ }
1067
+ body.focus();
1068
+ focusBlockStart(body.firstElementChild ?? body);
1069
+ setBodyActive(true);
1070
+ syncSelectionState();
1071
+ });
1072
+ }
1073
+ function updateHoveredBlockTools(clientX, clientY, body) {
1074
+ const element = document.elementFromPoint(clientX, clientY);
1075
+ if (element?.closest(".bayon-rte-special-tool")) {
1076
+ return;
1077
+ }
1078
+ const hoveredBlock = getHoveredEditorBlock(element);
1079
+ const hoveredBlockId = getHoveredEditorBlockActionTargetId(hoveredBlock, body) ??
1080
+ getHoveredBlockActionLaneId(clientX, clientY, body, blockActionToolPlacements);
1081
+ const specialBlockTarget = getSpecialBlockHoverTarget(hoveredBlock, body) ??
1082
+ (hoveredBlockId ? getSpecialBlockTargetById(hoveredBlockId, body) : null);
1083
+ setHoveredBlockId(hoveredBlockId);
1084
+ setSpecialBlockTarget(specialBlockTarget, body);
1085
+ }
1086
+ async function handleBlockAction(action, blockId) {
1087
+ const body = bodyRef.current;
1088
+ if (!body) {
1089
+ return;
1090
+ }
1091
+ const blocks = readBlocksFromEditor(body);
1092
+ const block = getBlockById(blocks, blockId);
1093
+ if (!block) {
1094
+ return;
1095
+ }
1096
+ if (action === "copy" || action === "cut") {
1097
+ const actionBlocks = getBlockActionBlocks(blocks, blockId);
1098
+ const clipboardText = actionBlocks.length
1099
+ ? encodeRichTextBlocksForClipboard(actionBlocks)
1100
+ : blockActionToClipboardText(blocks, blockId) ||
1101
+ blockToClipboardText(block);
1102
+ const copied = await writeTextToClipboard(clipboardText);
1103
+ if (!copied || action === "copy") {
1104
+ setActiveBlockActionMenuId(null);
1105
+ return;
1106
+ }
1107
+ }
1108
+ if (action === "cut" || action === "delete") {
1109
+ const deletionFocusTarget = getBlockDeletionFocusTarget(blocks, blockId);
1110
+ restoreScrollPositionAfter(window, () => applyContentBlocksChange(deleteBlockById(blocks, blockId), {
1111
+ focusBlockId: deletionFocusTarget?.blockId,
1112
+ focusBlockPosition: deletionFocusTarget?.position,
1113
+ historyLabel: "block-delete",
1114
+ }));
1115
+ }
1116
+ setActiveBlockActionMenuId(null);
1117
+ }
1118
+ function handleBlockPointerDragStart(blockId, event) {
1119
+ blockPointerDragCleanupRef.current?.();
1120
+ setDraggedBlockId(blockId);
1121
+ setBlockDragDropResult(null);
1122
+ setActiveBlockActionMenuId(null);
1123
+ blockPointerDragRef.current = {
1124
+ blockId,
1125
+ dropResult: null,
1126
+ started: false,
1127
+ x: event.clientX,
1128
+ y: event.clientY,
1129
+ };
1130
+ const handleWindowPointerMove = (windowEvent) => {
1131
+ handleBlockPointerDragMove(windowEvent);
1132
+ };
1133
+ const handleWindowPointerEnd = (windowEvent) => {
1134
+ handleBlockPointerDragEnd(windowEvent);
1135
+ };
1136
+ window.addEventListener("pointermove", handleWindowPointerMove);
1137
+ window.addEventListener("pointerup", handleWindowPointerEnd);
1138
+ window.addEventListener("pointercancel", handleWindowPointerEnd);
1139
+ blockPointerDragCleanupRef.current = () => {
1140
+ window.removeEventListener("pointermove", handleWindowPointerMove);
1141
+ window.removeEventListener("pointerup", handleWindowPointerEnd);
1142
+ window.removeEventListener("pointercancel", handleWindowPointerEnd);
1143
+ };
1144
+ }
1145
+ function handleBlockPointerDragMove(event) {
1146
+ const pointerDrag = blockPointerDragRef.current;
1147
+ const body = bodyRef.current;
1148
+ if (!pointerDrag || !body) {
1149
+ return;
1150
+ }
1151
+ const movedDistance = Math.hypot(event.clientX - pointerDrag.x, event.clientY - pointerDrag.y);
1152
+ if (movedDistance <= 3) {
1153
+ return;
1154
+ }
1155
+ if (!pointerDrag.started) {
1156
+ pointerDrag.started = true;
1157
+ }
1158
+ const bodyRect = body.getBoundingClientRect();
1159
+ const nextPlacements = getBlockActionToolPlacements(body);
1160
+ syncBlockActionToolPlacements(nextPlacements);
1161
+ pointerDrag.dropResult = getPointerDragDropResult(nextPlacements, pointerDrag.blockId, event.clientY - bodyRect.top + body.scrollTop, event.clientX - bodyRect.left + body.scrollLeft);
1162
+ setBlockDragDropResult(pointerDrag.dropResult);
1163
+ }
1164
+ function handleBlockPointerDragEnd(event) {
1165
+ const pointerDrag = blockPointerDragRef.current;
1166
+ const body = bodyRef.current;
1167
+ blockPointerDragCleanupRef.current?.();
1168
+ blockPointerDragCleanupRef.current = null;
1169
+ blockPointerDragRef.current = null;
1170
+ setDraggedBlockId(null);
1171
+ setBlockDragDropResult(null);
1172
+ if (!pointerDrag?.started ||
1173
+ !body ||
1174
+ pointerDrag.dropResult?.status !== "valid") {
1175
+ return;
1176
+ }
1177
+ event.preventDefault();
1178
+ applyContentBlocksChange(reorderBlock(readBlocksFromEditor(body), pointerDrag.blockId, pointerDrag.dropResult.dropTarget.targetBlockId, pointerDrag.dropResult.dropTarget.placement), { focusBody: false, historyLabel: "block-reorder" });
1179
+ }
1180
+ function applyContentBlocksChange(nextBlocks, options = {}, change) {
1181
+ commitEditorDocumentChange({
1182
+ ...options,
1183
+ change,
1184
+ nextContentBlocks: nextBlocks,
1185
+ });
1186
+ }
1187
+ function focusEditorBlock(body, blockId, position = "end") {
1188
+ const focusBlock = body.querySelector(`[data-block-id="${cssEscape(blockId)}"]`);
1189
+ if (focusBlock) {
1190
+ const treeRowOffset = position === "end" ? null : 0;
1191
+ const treeRow = getTreeRowEditorParts(focusBlock);
1192
+ if (treeRow) {
1193
+ focusTreeRowLabelAtTextOffset(focusBlock, treeRowOffset);
1194
+ }
1195
+ else {
1196
+ position === "end"
1197
+ ? focusBlockEnd(focusBlock)
1198
+ : focusBlockStart(focusBlock);
1199
+ }
1200
+ }
1201
+ else {
1202
+ focusBlockStart(body.firstElementChild ?? body);
1203
+ }
1204
+ }
1205
+ const showSpecialBlockTool = specialBlocksEnabled &&
1206
+ (!!hoveredBlockId || specialBlockToolHover || specialBlockToolOpen) &&
1207
+ !disabled &&
1208
+ specialBlockToolVisible;
1209
+ const visibleBlockActionToolPlacements = getVisibleBlockActionToolPlacements(blockActionToolPlacements, {
1210
+ activeBlockId: activeBlockActionMenuId,
1211
+ draggedBlockId,
1212
+ focusedBlockId,
1213
+ hoveredBlockId,
1214
+ });
1215
+ const showBlockActionTools = blockActionsEnabled &&
1216
+ !disabled &&
1217
+ !selectionActive &&
1218
+ visibleBlockActionToolPlacements.length > 0;
1219
+ const showBlockHoverZones = blockActionsEnabled && !disabled && !selectionActive;
1220
+ const uniqueBlockActionToolPlacements = getUniqueBlockActionToolPlacements(blockActionToolPlacements);
1221
+ const blockDragDropTarget = blockDragDropResult && "dropTarget" in blockDragDropResult
1222
+ ? blockDragDropResult.dropTarget
1223
+ : null;
1224
+ const blockDragIndicatorPosition = draggedBlockId && blockDragDropTarget
1225
+ ? getDragIndicatorPosition(blockActionToolPlacements, blockDragDropTarget)
1226
+ : null;
1227
+ return (_jsxs("div", { className: "bayon-rte-stack", children: [_jsx(RichTextStyleScope, {}), _jsxs(RichTextDocumentSurface, { background: documentBackground, children: [_jsx(RichTextTitleInput, { disabled: disabled, inputRef: titleRef, label: titleLabel, onArrowDown: focusBodyStart, onChange: handleTitleChange, onRedo: handleRedo, required: required, title: title, onUndo: handleUndo, validationMessage: titleValidationMessage }), _jsxs("div", { className: "bayon-rte-body-shell", onMouseDownCapture: (event) => {
1228
+ if (activeBlockActionMenuId &&
1229
+ event.target instanceof Element &&
1230
+ !event.target.closest("[data-block-action-tool]")) {
1231
+ setActiveBlockActionMenuId(null);
1232
+ }
1233
+ }, onMouseEnter: () => setBodyActive(true), onMouseLeave: () => {
1234
+ const body = bodyRef.current;
1235
+ const bodyStillFocused = !!body &&
1236
+ (document.activeElement === body ||
1237
+ body.contains(document.activeElement));
1238
+ if (!specialBlockToolHover &&
1239
+ !specialBlockToolOpen &&
1240
+ !blockActionToolHover &&
1241
+ !activeBlockActionMenuId &&
1242
+ !draggedBlockId &&
1243
+ !bodyStillFocused) {
1244
+ setBodyActive(false);
1245
+ }
1246
+ if (!activeBlockActionMenuId && !draggedBlockId) {
1247
+ setHoveredBlockId(null);
1248
+ setSpecialBlockTarget(null);
1249
+ setSpecialBlockToolOpen(false);
1250
+ }
1251
+ }, onMouseMove: (event) => {
1252
+ const body = bodyRef.current;
1253
+ updateBlockActionTools(body);
1254
+ if (body && blockActionsEnabled) {
1255
+ updateHoveredBlockTools(event.clientX, event.clientY, body);
1256
+ }
1257
+ }, children: [_jsx("input", { accept: "image/png,image/jpeg,image/webp", "aria-label": "Upload image", className: "bayon-rte-visually-hidden", disabled: disabled || !specialBlocksEnabled || !imageUpload, onChange: (event) => void handleImageFileChange(event.currentTarget.files?.[0] ?? null), ref: imageInputRef, type: "file" }), transcriptionNode, linkInputOpen && !disabled ? (_jsx(LinkCreationInput, { left: selectionToolbarPosition.left, onCancel: closeLinkInput, onSubmit: submitLinkInput, placement: selectionToolbarPosition.placement, top: selectionToolbarPosition.top })) : null, selectionActive && !linkInputOpen && !disabled ? (_jsx(SelectionFormatToolbar, { left: selectionToolbarPosition.left, onAction: runSelectionCommand, placement: selectionToolbarPosition.placement, top: selectionToolbarPosition.top })) : null, specialBlocksEnabled && !disabled ? (_jsx(SpecialBlockTool, { features: features, lockedFeatureMode: lockedFeatureMode, onFeatureGate: onFeatureGate, onHoverChange: setSpecialBlockToolHover, onInsert: insertBlock, onOpenChange: setSpecialBlockToolOpen, open: specialBlockToolOpen, top: specialBlockToolTop, toolHover: specialBlockToolHover, visible: showSpecialBlockTool })) : null, showBlockHoverZones
1258
+ ? uniqueBlockActionToolPlacements.map((placement) => (_jsx("div", { "aria-hidden": "true", className: `bayon-rte-block-hover-zone${placement.blockType === "image"
1259
+ ? " bayon-rte-block-hover-zone--image"
1260
+ : ""}`, onMouseEnter: () => {
1261
+ const body = bodyRef.current;
1262
+ setHoveredBlockId(placement.blockId);
1263
+ setSpecialBlockTarget(getSpecialBlockTargetById(placement.blockId, body), body);
1264
+ }, onMouseLeave: (event) => {
1265
+ if (event.relatedTarget instanceof Element &&
1266
+ event.relatedTarget.closest(".bayon-rte-special-tool")) {
1267
+ return;
1268
+ }
1269
+ if (!activeBlockActionMenuId && !draggedBlockId) {
1270
+ setHoveredBlockId(null);
1271
+ setSpecialBlockTarget(null);
1272
+ setSpecialBlockToolOpen(false);
1273
+ }
1274
+ }, style: {
1275
+ "--bayon-rte-block-hover-zone-height": `${Math.max(34, placement.bottom - placement.top)}px`,
1276
+ "--bayon-rte-block-hover-zone-left": `${placement.left}px`,
1277
+ "--bayon-rte-block-hover-zone-top": `${placement.top}px`,
1278
+ } }, `hover-${placement.blockId}`)))
1279
+ : null, showBlockActionTools
1280
+ ? visibleBlockActionToolPlacements.map((placement) => (_jsx(BlockActionTool, { activeBlockId: activeBlockActionMenuId, draggedBlockId: draggedBlockId, onAction: (action, blockId) => {
1281
+ void handleBlockAction(action, blockId);
1282
+ }, onHoverChange: setBlockActionToolHover, onPointerDragEnd: handleBlockPointerDragEnd, onPointerDragMove: handleBlockPointerDragMove, onPointerDragStart: handleBlockPointerDragStart, onToggleMenu: (blockId) => {
1283
+ setActiveBlockActionMenuId((currentBlockId) => {
1284
+ return currentBlockId === blockId ? null : blockId;
1285
+ });
1286
+ }, placement: placement }, placement.blockId)))
1287
+ : null, blockDragIndicatorPosition !== null ? (_jsx("div", { "aria-hidden": "true", className: `bayon-rte-block-drag-indicator${blockDragDropResult?.status === "invalid"
1288
+ ? " bayon-rte-block-drag-indicator--invalid"
1289
+ : ""}`, style: {
1290
+ "--bayon-rte-block-drag-indicator-left": `${blockDragIndicatorPosition.left}px`,
1291
+ "--bayon-rte-block-drag-indicator-top": `${blockDragIndicatorPosition.top}px`,
1292
+ } })) : null, _jsx(RichTextBody, { bodyRef: bodyRef, disabled: disabled, label: bodyLabel, onBackspace: handleBodyBackspace, onDelete: handleBodyDelete, onEnter: handleBodyEnter, onInput: handleBodyInput, onImageAlignChange: updateImageAlignment, onImageCustomWidthChange: (blockId, customWidth) => {
1293
+ updateImageDisplaySize(blockId, {
1294
+ customWidth,
1295
+ displaySize: "custom",
1296
+ });
1297
+ }, onImageSizeChange: (blockId, displaySize) => {
1298
+ const currentSizing = getCurrentImageSizing(blockId);
1299
+ const nextSizing = { displaySize };
1300
+ if (displaySize === "custom") {
1301
+ nextSizing.customWidth = normalizeImageCustomWidth(currentSizing.customWidth ?? IMAGE_CUSTOM_WIDTH_FALLBACK);
1302
+ }
1303
+ updateImageDisplaySize(blockId, nextSizing);
1304
+ }, onPasteText: handleBodyPasteText, onRedo: handleRedo, onSelectionChange: syncSelectionState, onShortcutCommand: runSelectionCommand, onTab: handleBodyTab, onToggleCollapse: handleToggleCollapse, onUndo: handleUndo, onFocus: () => {
1305
+ setBodyActive(true);
1306
+ syncSelectionState();
1307
+ } })] })] }), _jsxs("div", { className: "bayon-rte-meta", children: [helperText ? _jsx("small", { children: helperText }) : null, _jsxs("small", { children: [stats.wordCount, " words / ", stats.readingMinutes, " min read"] })] })] }));
1308
+ }
1309
+ async function writeTextToClipboard(value) {
1310
+ if (!value.trim()) {
1311
+ return false;
1312
+ }
1313
+ if (navigator.clipboard?.writeText) {
1314
+ try {
1315
+ await navigator.clipboard.writeText(value);
1316
+ return true;
1317
+ }
1318
+ catch {
1319
+ // Fall through to the textarea fallback below.
1320
+ }
1321
+ }
1322
+ const textarea = document.createElement("textarea");
1323
+ textarea.value = value;
1324
+ textarea.setAttribute("readonly", "");
1325
+ textarea.style.left = "-9999px";
1326
+ textarea.style.position = "fixed";
1327
+ textarea.style.top = "0";
1328
+ document.body.appendChild(textarea);
1329
+ textarea.select();
1330
+ try {
1331
+ return document.execCommand("copy");
1332
+ }
1333
+ finally {
1334
+ textarea.remove();
1335
+ }
1336
+ }
1337
+ async function readTextFromClipboard() {
1338
+ if (!navigator.clipboard?.readText) {
1339
+ return "";
1340
+ }
1341
+ try {
1342
+ return await navigator.clipboard.readText();
1343
+ }
1344
+ catch {
1345
+ return "";
1346
+ }
1347
+ }
1348
+ function isPasteSpecialBlockAction(action) {
1349
+ return "kind" in action && action.kind === "paste";
1350
+ }
1351
+ function isImageUploadSpecialBlockAction(action) {
1352
+ return "kind" in action && action.kind === "image-upload";
1353
+ }
1354
+ function wrapSelectionWith(tagName) {
1355
+ const selection = window.getSelection();
1356
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
1357
+ return;
1358
+ }
1359
+ const range = selection.getRangeAt(0);
1360
+ const wrapper = document.createElement(tagName);
1361
+ wrapper.appendChild(range.extractContents());
1362
+ range.insertNode(wrapper);
1363
+ selection.removeAllRanges();
1364
+ const nextRange = document.createRange();
1365
+ nextRange.selectNodeContents(wrapper);
1366
+ selection.addRange(nextRange);
1367
+ }
1368
+ function getVisibleRangeRect(range) {
1369
+ const rect = range.getBoundingClientRect();
1370
+ if (rect.width || rect.height) {
1371
+ return rect;
1372
+ }
1373
+ return (Array.from(range.getClientRects()).find((clientRect) => {
1374
+ return clientRect.width || clientRect.height;
1375
+ }) ?? rect);
1376
+ }
1377
+ function getActiveEditorBlock(range, body) {
1378
+ const node = range.startContainer.nodeType === Node.TEXT_NODE
1379
+ ? range.startContainer.parentElement
1380
+ : range.startContainer instanceof Element
1381
+ ? range.startContainer
1382
+ : null;
1383
+ const block = node?.closest("[data-block-id], h2, blockquote, pre, figure, hr");
1384
+ return block && body.contains(block) ? block : null;
1385
+ }
1386
+ function isDirectToggleContentChild(block) {
1387
+ return block.parentElement?.hasAttribute("data-toggle-content") === true;
1388
+ }
1389
+ function getBlockInsertTop(block, body) {
1390
+ if (!block) {
1391
+ return null;
1392
+ }
1393
+ const bodyRect = body.getBoundingClientRect();
1394
+ const blockRect = block.getBoundingClientRect();
1395
+ if (!blockRect.height && !blockRect.width) {
1396
+ return null;
1397
+ }
1398
+ return Math.max(0, blockRect.top - bodyRect.top + body.scrollTop);
1399
+ }
1400
+ function getBlockActionToolPlacements(body) {
1401
+ const placements = [];
1402
+ const editorBlocks = Array.from(body.querySelectorAll("[data-block-id]")).filter(isReadableEditorBlock);
1403
+ for (const element of editorBlocks) {
1404
+ const block = readBlockFromElement(element);
1405
+ if (!element.getAttribute("data-block-id")) {
1406
+ ensureEditorBlockId(element, block.id);
1407
+ }
1408
+ const position = getBlockPosition(getBlockActionAnchorElement(element), body);
1409
+ const ancestorBlockIds = getBlockAncestorIds(element, body);
1410
+ if (position && isBlockActionTarget(block)) {
1411
+ placements.push({
1412
+ ancestorBlockIds,
1413
+ blockId: block.id,
1414
+ blockType: block.type,
1415
+ bottom: position.bottom,
1416
+ depth: ancestorBlockIds.length,
1417
+ left: position.left,
1418
+ top: position.top,
1419
+ toolTop: getBlockActionToolAnchorTop(block.type, position.top),
1420
+ });
1421
+ }
1422
+ if (block.type === "toggle") {
1423
+ const contentPosition = getBlockPosition(getDirectToggleContentElement(element), body);
1424
+ if (contentPosition) {
1425
+ placements.push({
1426
+ blockId: block.id,
1427
+ bottom: contentPosition.bottom,
1428
+ dropRole: "content",
1429
+ left: contentPosition.left,
1430
+ top: contentPosition.top,
1431
+ });
1432
+ }
1433
+ }
1434
+ }
1435
+ return placements;
1436
+ }
1437
+ function getBlockPosition(block, body) {
1438
+ if (!block) {
1439
+ return null;
1440
+ }
1441
+ const bodyRect = body.getBoundingClientRect();
1442
+ const blockRect = block.getBoundingClientRect();
1443
+ if (!blockRect.height && !blockRect.width) {
1444
+ return null;
1445
+ }
1446
+ const top = Math.max(0, blockRect.top - bodyRect.top + body.scrollTop);
1447
+ const left = Math.max(0, blockRect.left - bodyRect.left + body.scrollLeft);
1448
+ return {
1449
+ bottom: top + blockRect.height,
1450
+ left,
1451
+ top,
1452
+ };
1453
+ }
1454
+ function getBlockAncestorIds(block, body) {
1455
+ const ancestorBlockIds = [];
1456
+ let ancestor = block.parentElement?.closest("[data-block-id]") ?? null;
1457
+ while (ancestor && body.contains(ancestor)) {
1458
+ const blockId = ancestor.getAttribute("data-block-id");
1459
+ if (blockId) {
1460
+ ancestorBlockIds.unshift(blockId);
1461
+ }
1462
+ ancestor = ancestor.parentElement?.closest("[data-block-id]") ?? null;
1463
+ }
1464
+ return ancestorBlockIds;
1465
+ }
1466
+ function getBlockActionAnchorElement(block) {
1467
+ const richTextBlock = readBlockFromElement(block);
1468
+ const anchorSelector = getBlockActionAnchorSelector(richTextBlock.type);
1469
+ const anchor = anchorSelector ? block.closest(anchorSelector) : null;
1470
+ return (anchor ??
1471
+ getTreeRowEditorParts(block)?.label.closest("[data-rich-text-row]") ??
1472
+ block);
1473
+ }
1474
+ export function getBlockActionAnchorSelector(blockType) {
1475
+ return blockType === "image" ? "[data-rich-text-image]" : "";
1476
+ }
1477
+ const nestedDropIndicatorIndent = 32;
1478
+ export function getDragIndicatorPosition(placements, dropTarget) {
1479
+ const targetPlacement = dropTarget.placement === "inside"
1480
+ ? (placements.find((placement) => {
1481
+ return (placement.blockId === dropTarget.targetBlockId &&
1482
+ placement.dropRole === "content");
1483
+ }) ??
1484
+ placements.find((placement) => {
1485
+ return placement.blockId === dropTarget.targetBlockId;
1486
+ }))
1487
+ : placements.find((placement) => {
1488
+ return (placement.blockId === dropTarget.targetBlockId &&
1489
+ placement.dropRole !== "content");
1490
+ });
1491
+ if (!targetPlacement) {
1492
+ return null;
1493
+ }
1494
+ if (dropTarget.placement === "inside") {
1495
+ return {
1496
+ left: targetPlacement.dropRole === "content"
1497
+ ? targetPlacement.left
1498
+ : targetPlacement.left + nestedDropIndicatorIndent,
1499
+ top: targetPlacement.bottom,
1500
+ };
1501
+ }
1502
+ return {
1503
+ left: targetPlacement.left,
1504
+ top: dropTarget.placement === "after"
1505
+ ? targetPlacement.bottom
1506
+ : targetPlacement.top,
1507
+ };
1508
+ }
1509
+ function getHoveredEditorBlockActionTargetId(block, body) {
1510
+ if (!block || !body.contains(block) || !isReadableEditorBlock(block)) {
1511
+ return null;
1512
+ }
1513
+ const richTextBlock = readBlockFromElement(block);
1514
+ return isBlockActionTarget(richTextBlock) ? richTextBlock.id : null;
1515
+ }
1516
+ function getSpecialBlockHoverTarget(block, body) {
1517
+ if (!block || !body.contains(block) || !isReadableEditorBlock(block)) {
1518
+ return null;
1519
+ }
1520
+ return isSpecialBlockInsertTarget(block) &&
1521
+ !block.hasAttribute("data-suppress-special-block-tool")
1522
+ ? block
1523
+ : null;
1524
+ }
1525
+ function getSpecialBlockTargetById(blockId, body) {
1526
+ if (!body) {
1527
+ return null;
1528
+ }
1529
+ return getSpecialBlockHoverTarget(body.querySelector(`[data-block-id="${cssEscape(blockId)}"]`), body);
1530
+ }
1531
+ function getHoveredEditorBlock(element) {
1532
+ const treeRowBlock = element?.closest("[data-rich-text-row]")?.parentElement;
1533
+ if (treeRowBlock?.hasAttribute("data-block-id") === true) {
1534
+ return treeRowBlock;
1535
+ }
1536
+ const toggleContentBlock = element?.closest("[data-toggle-content] > [data-block-id]");
1537
+ if (toggleContentBlock) {
1538
+ return toggleContentBlock;
1539
+ }
1540
+ return element?.closest("p, div[data-block-id], h2, blockquote, pre, figure, hr, ul, ol");
1541
+ }
1542
+ function getHoveredBlockActionLaneId(clientX, clientY, body, placements) {
1543
+ const bodyRect = body.getBoundingClientRect();
1544
+ const pointerTop = clientY - bodyRect.top + body.scrollTop;
1545
+ const pointerLeft = clientX - bodyRect.left + body.scrollLeft;
1546
+ return getPointerHoverLaneBlockId(placements, pointerTop, pointerLeft);
1547
+ }
1548
+ function isCheckboxEditorBlock(element) {
1549
+ return !!element?.hasAttribute("data-rich-text-checkbox");
1550
+ }
1551
+ function isNestableEditorBlock(element) {
1552
+ return (!!element?.hasAttribute("data-rich-text-bullet") ||
1553
+ !!element?.hasAttribute("data-rich-text-ordered") ||
1554
+ !!element?.hasAttribute("data-rich-text-checkbox") ||
1555
+ !!element?.hasAttribute("data-rich-text-toggle"));
1556
+ }
1557
+ function getTreeRowEditorParts(element) {
1558
+ if (!element) {
1559
+ return null;
1560
+ }
1561
+ let type = null;
1562
+ if (element.hasAttribute("data-rich-text-bullet")) {
1563
+ type = "bullet";
1564
+ }
1565
+ else if (element.hasAttribute("data-rich-text-ordered")) {
1566
+ type = "ordered";
1567
+ }
1568
+ else if (element.hasAttribute("data-rich-text-checkbox")) {
1569
+ type = "checkbox";
1570
+ }
1571
+ else if (element.hasAttribute("data-rich-text-toggle")) {
1572
+ type = "toggle";
1573
+ }
1574
+ if (!type) {
1575
+ return null;
1576
+ }
1577
+ const label = getDirectTreeRowLabel(element, type);
1578
+ return label ? { label, type } : null;
1579
+ }
1580
+ function focusTreeRowLabelAtTextOffset(block, offset) {
1581
+ const treeRow = getTreeRowEditorParts(block);
1582
+ if (!treeRow) {
1583
+ focusBlockEnd(block);
1584
+ return;
1585
+ }
1586
+ if (offset === null) {
1587
+ focusBlockEnd(treeRow.label);
1588
+ return;
1589
+ }
1590
+ focusBlockAtTextOffset(treeRow.label, offset);
1591
+ }
1592
+ export function getTreeRowBackspaceMarkdown(type) {
1593
+ if (type === "ordered") {
1594
+ return "1.";
1595
+ }
1596
+ if (type === "toggle") {
1597
+ return "";
1598
+ }
1599
+ if (type === "checkbox") {
1600
+ return "[ ]";
1601
+ }
1602
+ return "*";
1603
+ }
1604
+ export function getTreeRowBackspaceFocusPosition(type) {
1605
+ return getTreeRowBackspaceMarkdown(type) ? "end" : "start";
1606
+ }
1607
+ export function getTreeRowBackspaceParagraphMarkdown(type, currentMarkdown) {
1608
+ if (currentMarkdown.trim().length > 0) {
1609
+ return currentMarkdown;
1610
+ }
1611
+ if (type === "checkbox") {
1612
+ return "";
1613
+ }
1614
+ return getTreeRowBackspaceMarkdown(type);
1615
+ }
1616
+ export function getTreeRowBackspaceFocusPositionForMarkdown(markdown) {
1617
+ return markdown ? "end" : "start";
1618
+ }
1619
+ export function getTreeRowBackspaceFocusPositionForParagraph(type, currentMarkdown) {
1620
+ if (currentMarkdown.trim().length > 0) {
1621
+ return "start";
1622
+ }
1623
+ return type === "bullet" || type === "ordered" ? "end" : "start";
1624
+ }
1625
+ export function canBackspaceEmptyToggleTitle(block) {
1626
+ return (block?.type === "toggle" && isRichTextBlocksEmpty(block.children ?? []));
1627
+ }
1628
+ function isCaretInDeletableBlockContent(block, range) {
1629
+ const treeRow = getTreeRowEditorParts(block);
1630
+ if (treeRow) {
1631
+ return treeRow.label.contains(range.startContainer);
1632
+ }
1633
+ return isSoftBreakTextBlock(block) || block.tagName === "PRE";
1634
+ }
1635
+ function isEmptyEditableBlockWithoutChildren(block) {
1636
+ if (!block) {
1637
+ return false;
1638
+ }
1639
+ if (isNestableRichTextBlock(block) && (block.children?.length ?? 0) > 0) {
1640
+ return false;
1641
+ }
1642
+ if (block.type === "code") {
1643
+ return block.text.trim().length === 0;
1644
+ }
1645
+ if (block.type === "paragraph" ||
1646
+ block.type === "heading" ||
1647
+ block.type === "quote" ||
1648
+ block.type === "bullet" ||
1649
+ block.type === "ordered" ||
1650
+ block.type === "checkbox" ||
1651
+ block.type === "toggle") {
1652
+ return block.markdown.trim().length === 0;
1653
+ }
1654
+ return false;
1655
+ }
1656
+ function getToggleContentPlaceholderParentId(block) {
1657
+ if (!block?.hasAttribute("data-toggle-content-placeholder-block")) {
1658
+ return null;
1659
+ }
1660
+ return (block
1661
+ .closest("[data-rich-text-toggle][data-block-id]")
1662
+ ?.getAttribute("data-block-id") ?? null);
1663
+ }
1664
+ function isSoftBreakTextBlock(element) {
1665
+ return ["P", "H2", "BLOCKQUOTE"].includes(element.tagName);
1666
+ }
1667
+ function isEditorBlockAtBackspaceStart(block, range) {
1668
+ const treeRow = getTreeRowEditorParts(block);
1669
+ if (treeRow) {
1670
+ return (treeRow.label.contains(range.startContainer) &&
1671
+ getTextOffsetWithin(treeRow.label, range) === 0);
1672
+ }
1673
+ return isSoftBreakTextBlock(block) && getTextOffsetWithin(block, range) === 0;
1674
+ }
1675
+ function isEmptyTextBlockAtStart(block, range) {
1676
+ return (isSoftBreakTextBlock(block) &&
1677
+ getTextOffsetWithin(block, range) === 0 &&
1678
+ editorHtmlToMarkdown(normalizeSoftBreakHtml(block.innerHTML)).trim()
1679
+ .length === 0);
1680
+ }
1681
+ function getBlockById(blocks, blockId) {
1682
+ const path = findBlockPath(blocks, blockId);
1683
+ return path ? getBlockAtPath(blocks, path) : null;
1684
+ }
1685
+ export function areBlockActionToolPlacementsEqual(previousPlacements, nextPlacements) {
1686
+ return (previousPlacements.length === nextPlacements.length &&
1687
+ previousPlacements.every((placement, index) => {
1688
+ const nextPlacement = nextPlacements[index];
1689
+ return (placement.blockId === nextPlacement?.blockId &&
1690
+ placement.blockType === nextPlacement.blockType &&
1691
+ placement.top === nextPlacement.top &&
1692
+ placement.bottom === nextPlacement.bottom &&
1693
+ placement.left === nextPlacement.left &&
1694
+ placement.toolTop === nextPlacement.toolTop);
1695
+ }));
1696
+ }
1697
+ export function getDeletionFocusTarget(blocks, blockId) {
1698
+ return getBlockDeletionFocusTarget(blocks, blockId);
1699
+ }
1700
+ function focusBlockStart(block) {
1701
+ if (!block) {
1702
+ return;
1703
+ }
1704
+ const range = document.createRange();
1705
+ range.setStart(block, 0);
1706
+ range.collapse(true);
1707
+ const selection = window.getSelection();
1708
+ selection?.removeAllRanges();
1709
+ selection?.addRange(range);
1710
+ }
1711
+ function selectBlockContents(block) {
1712
+ if (!block) {
1713
+ return;
1714
+ }
1715
+ const selectionTarget = block.querySelector("p, code") ?? block;
1716
+ const range = document.createRange();
1717
+ range.selectNodeContents(selectionTarget);
1718
+ const selection = window.getSelection();
1719
+ selection?.removeAllRanges();
1720
+ selection?.addRange(range);
1721
+ }
1722
+ function isSpecialBlockInsertTarget(block) {
1723
+ if (!block || !["DIV", "P"].includes(block.tagName)) {
1724
+ return false;
1725
+ }
1726
+ if (block.hasAttribute("data-rich-text-checkbox")) {
1727
+ return false;
1728
+ }
1729
+ return Array.from(block.childNodes).every((child) => {
1730
+ if (child.nodeType === Node.TEXT_NODE) {
1731
+ return (child.textContent ?? "").trim() === "";
1732
+ }
1733
+ if (child.nodeType !== Node.ELEMENT_NODE) {
1734
+ return true;
1735
+ }
1736
+ const element = child;
1737
+ return (element.tagName === "BR" || (element.textContent ?? "").trim() === "");
1738
+ });
1739
+ }
1740
+ function clamp(value, min, max) {
1741
+ return Math.min(Math.max(value, min), max);
1742
+ }
1743
+ export function getEditorHtmlForRichTextBlocks(blocks) {
1744
+ return richTextBlocksToEditorHtml(blocks);
1745
+ }
1746
+ export function buildUploadedImageBlock(upload, options = {}) {
1747
+ const url = getSafeImageUrl(upload.url);
1748
+ if (!url) {
1749
+ throw new Error("Uploaded image URL is invalid.");
1750
+ }
1751
+ return {
1752
+ alignment: options.alignment ?? "center",
1753
+ id: options.id ?? createRichTextBlockId(),
1754
+ type: "image",
1755
+ url,
1756
+ ...(upload.alt?.trim() ? { alt: upload.alt.trim() } : {}),
1757
+ };
1758
+ }
1759
+ export function buildUploadingImageBlock(input) {
1760
+ const progress = normalizeUploadProgress(input.progress ?? 0);
1761
+ return {
1762
+ alignment: input.alignment ?? "center",
1763
+ id: input.id ?? createRichTextBlockId(),
1764
+ type: "image",
1765
+ alt: getImageUploadProgressMessage(progress),
1766
+ uploadProgress: progress,
1767
+ };
1768
+ }
1769
+ export function getImageUploadProgressMessage(progress) {
1770
+ return `Uploading image... ${normalizeUploadProgress(progress)}%`;
1771
+ }
1772
+ export function getImageUploadFailureMessage(error) {
1773
+ const message = error instanceof Error ? error.message : "";
1774
+ if (message.includes("storage/unauthorized")) {
1775
+ return "Image upload failed. Check that you are signed in and try again.";
1776
+ }
1777
+ return message
1778
+ ? `Image upload failed. ${message}`
1779
+ : "Image upload failed. Try again.";
1780
+ }
1781
+ function normalizeUploadProgress(progress) {
1782
+ return clamp(Math.round(progress), 0, 100);
1783
+ }
1784
+ function replaceBlockById(blocks, blockId, replacementBlock) {
1785
+ const path = findBlockPath(blocks, blockId);
1786
+ if (!path) {
1787
+ return blocks;
1788
+ }
1789
+ return updateBlockAtPath(blocks, path, () => replacementBlock);
1790
+ }
1791
+ function getImageAlignment(alignment) {
1792
+ return alignment === "left" || alignment === "right" ? alignment : "center";
1793
+ }
1794
+ function getImageDisplaySize(displaySize) {
1795
+ return displaySize === "small" ||
1796
+ displaySize === "medium" ||
1797
+ displaySize === "full" ||
1798
+ displaySize === "custom"
1799
+ ? displaySize
1800
+ : null;
1801
+ }
1802
+ function normalizeImageCustomWidth(width) {
1803
+ if (typeof width !== "number" || !Number.isFinite(width)) {
1804
+ return IMAGE_CUSTOM_WIDTH_FALLBACK;
1805
+ }
1806
+ return clamp(Math.round(width), 120, 1600);
1807
+ }
1808
+ function getImageDisplayAttributes(block) {
1809
+ const displaySize = getImageDisplaySize(block.displaySize);
1810
+ const customWidth = displaySize === "custom"
1811
+ ? normalizeImageCustomWidth(block.customWidth)
1812
+ : null;
1813
+ return {
1814
+ customWidth,
1815
+ displaySize,
1816
+ html: (displaySize ? ` data-image-size="${displaySize}"` : "") +
1817
+ (customWidth ? ` data-image-custom-width="${customWidth}"` : "") +
1818
+ (customWidth
1819
+ ? ` style="--bayon-rte-image-custom-width:${customWidth}px"`
1820
+ : ""),
1821
+ };
1822
+ }
1823
+ function imageToolbarToEditorHtml(activeAlignment, activeDisplaySize, customWidth) {
1824
+ return `<div class="bayon-rte-image-align-tool" contenteditable="false" data-image-align-tool="" role="toolbar" aria-label="Image layout">${imageAlignmentButtonToEditorHtml("left", activeAlignment)}${imageAlignmentButtonToEditorHtml("center", activeAlignment)}${imageAlignmentButtonToEditorHtml("right", activeAlignment)}<span aria-hidden="true" class="bayon-rte-image-toolbar-divider"></span>${imageSizeButtonToEditorHtml("small", activeDisplaySize)}${imageSizeButtonToEditorHtml("medium", activeDisplaySize)}${imageSizeButtonToEditorHtml("full", activeDisplaySize)}${imageSizeButtonToEditorHtml("custom", activeDisplaySize)}${imageCustomWidthInputToEditorHtml(customWidth)}</div>`;
1825
+ }
1826
+ function imageAlignmentButtonToEditorHtml(alignment, activeAlignment) {
1827
+ const isActive = alignment === activeAlignment;
1828
+ return `<button aria-label="Align image ${alignment}" class="bayon-rte-image-align-button" contenteditable="false" data-image-align-action="${alignment}"${isActive ? ' data-image-align-active="true"' : ""} title="Align image ${alignment}" type="button"><span aria-hidden="true" class="bayon-rte-image-align-icon bayon-rte-image-align-icon--${alignment}"><span></span><span></span><span></span></span></button>`;
1829
+ }
1830
+ function imageSizeButtonToEditorHtml(displaySize, activeDisplaySize) {
1831
+ const isActive = displaySize === activeDisplaySize;
1832
+ const label = displaySize === "custom"
1833
+ ? "Set custom image size"
1834
+ : `Set image size ${displaySize}`;
1835
+ const iconLabel = displaySize === "small"
1836
+ ? "S"
1837
+ : displaySize === "medium"
1838
+ ? "M"
1839
+ : displaySize === "full"
1840
+ ? "F"
1841
+ : "W";
1842
+ return `<button aria-label="${label}" class="bayon-rte-image-align-button bayon-rte-image-size-button" contenteditable="false" data-image-size-action="${displaySize}"${isActive ? ' data-image-size-active="true"' : ""} title="${label}" type="button"><span aria-hidden="true">${iconLabel}</span></button>`;
1843
+ }
1844
+ function imageCustomWidthInputToEditorHtml(customWidth) {
1845
+ const value = customWidth ?? IMAGE_CUSTOM_WIDTH_FALLBACK;
1846
+ return `<input aria-label="Custom image width" class="bayon-rte-image-custom-width-input" contenteditable="false" data-image-custom-width-action="" inputmode="numeric" max="1600" min="120" title="Custom image width" type="number" value="${value}">`;
1847
+ }
1848
+ function richTextBlocksToEditorHtml(blocks) {
1849
+ const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
1850
+ const parts = [];
1851
+ for (let index = 0; index < sanitizedBlocks.length; index += 1) {
1852
+ const block = sanitizedBlocks[index];
1853
+ if (!block) {
1854
+ continue;
1855
+ }
1856
+ if (isTreeEditorBlock(block)) {
1857
+ const treeBlocks = [];
1858
+ const treeKind = getTreeListKind(block);
1859
+ while (true) {
1860
+ const treeBlock = sanitizedBlocks[index];
1861
+ if (!isTreeEditorBlock(treeBlock) ||
1862
+ getTreeListKind(treeBlock) !== treeKind) {
1863
+ break;
1864
+ }
1865
+ treeBlocks.push(treeBlock);
1866
+ index += 1;
1867
+ }
1868
+ index -= 1;
1869
+ parts.push(treeEditorBlocksToHtml(treeBlocks, treeKind));
1870
+ continue;
1871
+ }
1872
+ parts.push(richTextBlockToEditorHtml(block));
1873
+ }
1874
+ return parts.join("");
1875
+ }
1876
+ function richTextBlockToEditorHtml(block) {
1877
+ const blockId = escapeHtmlAttribute(block.id);
1878
+ if (block.type === "heading") {
1879
+ return `<h2 data-block-id="${blockId}">${textBlockMarkdownToEditorHtml(block.markdown)}</h2>`;
1880
+ }
1881
+ if (block.type === "quote") {
1882
+ return `<blockquote data-block-id="${blockId}">${textBlockMarkdownToEditorHtml(block.markdown)}</blockquote>`;
1883
+ }
1884
+ if (block.type === "message") {
1885
+ return `<p data-block-id="${blockId}" data-rich-text-message="">${textBlockMarkdownToEditorHtml(block.markdown)}</p>`;
1886
+ }
1887
+ if (block.type === "code") {
1888
+ return `<pre data-block-id="${blockId}"><code>${escapeHtmlText(block.text)}</code></pre>`;
1889
+ }
1890
+ if (block.type === "divider") {
1891
+ return `<hr data-block-id="${blockId}">`;
1892
+ }
1893
+ if (block.type === "checkbox") {
1894
+ return checkboxBlockToEditorHtml(block);
1895
+ }
1896
+ if (block.type === "bullet") {
1897
+ return listBlockToEditorHtml(block);
1898
+ }
1899
+ if (block.type === "ordered") {
1900
+ return orderedBlockToEditorHtml(block);
1901
+ }
1902
+ if (block.type === "toggle") {
1903
+ return toggleBlockToEditorHtml(block);
1904
+ }
1905
+ if (block.type === "image") {
1906
+ const alignment = getImageAlignment(block.alignment);
1907
+ const alt = escapeHtmlAttribute(block.alt ?? "");
1908
+ const displayAttributes = getImageDisplayAttributes(block);
1909
+ const url = block.url ? escapeHtmlAttribute(block.url) : "";
1910
+ const uploadProgress = typeof block.uploadProgress === "number"
1911
+ ? normalizeUploadProgress(block.uploadProgress)
1912
+ : null;
1913
+ const placeholderText = escapeHtmlText(block.alt ?? "Image placeholder");
1914
+ const toolbarHtml = imageToolbarToEditorHtml(alignment, displayAttributes.displaySize, displayAttributes.customWidth);
1915
+ if (url) {
1916
+ return `<figure contenteditable="false" data-block-id="${blockId}" data-rich-text-image="" data-image-align="${alignment}"${displayAttributes.html}>${toolbarHtml}<img src="${url}" alt="${alt}"></figure>`;
1917
+ }
1918
+ if (uploadProgress !== null) {
1919
+ return `<figure contenteditable="false" data-block-id="${blockId}" data-rich-text-image="" data-image-align="${alignment}"${displayAttributes.html} data-image-upload-progress="${uploadProgress}" data-placeholder="image">${toolbarHtml}<p>${placeholderText}</p></figure>`;
1920
+ }
1921
+ return `<figure data-block-id="${blockId}" data-rich-text-image="" data-image-align="${alignment}"${displayAttributes.html} data-placeholder="image">${toolbarHtml}</figure>`;
1922
+ }
1923
+ if (block.type === "tool") {
1924
+ return toolBlockToEditorHtml(block);
1925
+ }
1926
+ return `<p data-block-id="${blockId}">${textBlockMarkdownToEditorHtml(block.markdown)}</p>`;
1927
+ }
1928
+ function toolBlockToEditorHtml(block) {
1929
+ const blockId = escapeHtmlAttribute(block.id);
1930
+ const toolType = escapeHtmlAttribute(block.toolType);
1931
+ const status = escapeHtmlAttribute(block.status);
1932
+ const title = block.title ?? "";
1933
+ const markdown = block.markdown ?? "";
1934
+ const data = block.data === undefined ? "" : JSON.stringify(block.data);
1935
+ const titleAttribute = title
1936
+ ? ` data-tool-title="${escapeHtmlAttribute(title)}"`
1937
+ : "";
1938
+ const markdownAttribute = markdown
1939
+ ? ` data-tool-markdown="${escapeHtmlAttribute(encodeURIComponent(markdown))}"`
1940
+ : "";
1941
+ const dataAttribute = data
1942
+ ? ` data-tool-data="${escapeHtmlAttribute(encodeURIComponent(data))}"`
1943
+ : "";
1944
+ const label = title || block.toolType;
1945
+ const markdownHtml = markdown
1946
+ ? `<p data-tool-markdown="">${markdownToEditorHtml(markdown)}</p>`
1947
+ : "";
1948
+ return `<section contenteditable="false" data-block-id="${blockId}" data-rich-text-tool="" data-tool-type="${toolType}" data-tool-status="${status}"${titleAttribute}${markdownAttribute}${dataAttribute}><div data-tool-header=""><span data-tool-title="">${escapeHtmlText(label)}</span><span data-tool-status-label="">${escapeHtmlText(block.status)}</span></div>${markdownHtml}</section>`;
1949
+ }
1950
+ function textBlockMarkdownToEditorHtml(markdown) {
1951
+ return markdownToEditorHtml(markdown) || "&nbsp;";
1952
+ }
1953
+ function textBlockMarkdownToEditorHtmlWithPlaceholder(markdown) {
1954
+ return markdown.trim() ? markdownToEditorHtml(markdown) : "";
1955
+ }
1956
+ function readBlocksFromEditor(body) {
1957
+ const blocks = Array.from(body.childNodes).flatMap((node) => {
1958
+ if (node.nodeType === Node.TEXT_NODE) {
1959
+ const markdown = editorHtmlToMarkdown(node.textContent ?? "");
1960
+ return markdown
1961
+ ? [
1962
+ {
1963
+ id: createRichTextBlockId(),
1964
+ type: "paragraph",
1965
+ markdown,
1966
+ },
1967
+ ]
1968
+ : [];
1969
+ }
1970
+ if (!(node instanceof Element)) {
1971
+ return [];
1972
+ }
1973
+ if (isEditorListContainer(node)) {
1974
+ return readEditorListBlocks(node);
1975
+ }
1976
+ if (isReadableEditorBlock(node)) {
1977
+ return [readBlockFromElement(node)];
1978
+ }
1979
+ const markdown = editorHtmlToMarkdown(normalizeSoftBreakHtml(node.innerHTML || node.textContent || ""));
1980
+ return markdown
1981
+ ? [
1982
+ {
1983
+ id: createRichTextBlockId(),
1984
+ type: "paragraph",
1985
+ markdown,
1986
+ },
1987
+ ]
1988
+ : [];
1989
+ });
1990
+ if (blocks.length === 0 && (body.textContent ?? "").trim()) {
1991
+ return sanitizeRichTextBlocks([
1992
+ {
1993
+ id: createRichTextBlockId(),
1994
+ type: "paragraph",
1995
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(body.innerHTML)),
1996
+ },
1997
+ ]);
1998
+ }
1999
+ return sanitizeRichTextBlocks(blocks);
2000
+ }
2001
+ function isReadableEditorBlock(element) {
2002
+ return [
2003
+ "P",
2004
+ "DIV",
2005
+ "LI",
2006
+ "H2",
2007
+ "BLOCKQUOTE",
2008
+ "PRE",
2009
+ "FIGURE",
2010
+ "SECTION",
2011
+ "HR",
2012
+ "LABEL",
2013
+ ].includes(element.tagName);
2014
+ }
2015
+ function readBlockFromElement(element) {
2016
+ const id = element.getAttribute("data-block-id") || createRichTextBlockId();
2017
+ const tagName = element.tagName.toLowerCase();
2018
+ if (tagName === "h2") {
2019
+ return {
2020
+ id,
2021
+ type: "heading",
2022
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(element.innerHTML)),
2023
+ };
2024
+ }
2025
+ if (tagName === "blockquote") {
2026
+ return {
2027
+ id,
2028
+ type: "quote",
2029
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(element.innerHTML)),
2030
+ };
2031
+ }
2032
+ if (element.hasAttribute("data-rich-text-message")) {
2033
+ return {
2034
+ id,
2035
+ type: "message",
2036
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(element.innerHTML)),
2037
+ };
2038
+ }
2039
+ if (element.hasAttribute("data-rich-text-tool")) {
2040
+ return readToolBlockFromElement(element, id);
2041
+ }
2042
+ if (tagName === "pre") {
2043
+ return { id, type: "code", text: element.textContent ?? "" };
2044
+ }
2045
+ if (tagName === "hr") {
2046
+ return { id, type: "divider" };
2047
+ }
2048
+ if ((tagName === "div" || tagName === "li") &&
2049
+ element.hasAttribute("data-rich-text-checkbox")) {
2050
+ const input = element.querySelector("input[type='checkbox']");
2051
+ const label = getDirectTreeRowLabel(element, "checkbox");
2052
+ return {
2053
+ id,
2054
+ type: "checkbox",
2055
+ checked: input instanceof HTMLInputElement ? input.checked : false,
2056
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(label?.innerHTML ?? element.innerHTML)),
2057
+ ...readEditorChildren(element),
2058
+ };
2059
+ }
2060
+ if ((tagName === "div" || tagName === "li") &&
2061
+ element.hasAttribute("data-rich-text-bullet")) {
2062
+ return {
2063
+ id,
2064
+ type: "bullet",
2065
+ markdown: getBulletMarkdownFromElement(element),
2066
+ ...readEditorChildren(element),
2067
+ };
2068
+ }
2069
+ if ((tagName === "div" || tagName === "li") &&
2070
+ element.hasAttribute("data-rich-text-ordered")) {
2071
+ return {
2072
+ id,
2073
+ type: "ordered",
2074
+ markdown: getOrderedMarkdownFromElement(element),
2075
+ ...readEditorChildren(element),
2076
+ };
2077
+ }
2078
+ if ((tagName === "div" || tagName === "li") &&
2079
+ element.hasAttribute("data-rich-text-toggle")) {
2080
+ const label = getDirectTreeRowLabel(element, "toggle");
2081
+ return {
2082
+ id,
2083
+ type: "toggle",
2084
+ collapsed: element.getAttribute("data-toggle-collapsed") === "true",
2085
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(label?.innerHTML ?? element.innerHTML)),
2086
+ children: readToggleChildBlocks(element),
2087
+ };
2088
+ }
2089
+ if (tagName === "figure") {
2090
+ const image = element.querySelector("img");
2091
+ const url = image instanceof HTMLImageElement ? getSafeImageUrl(image.src) : "";
2092
+ const uploadProgress = readImageUploadProgress(element);
2093
+ const displaySize = getImageDisplaySize(element.getAttribute("data-image-size"));
2094
+ const customWidth = readImageCustomWidth(element);
2095
+ return {
2096
+ id,
2097
+ type: "image",
2098
+ alignment: getImageAlignment(element.getAttribute("data-image-align")),
2099
+ ...(displaySize ? { displaySize } : {}),
2100
+ ...(displaySize === "custom" && customWidth ? { customWidth } : {}),
2101
+ ...(url ? { url } : {}),
2102
+ ...(uploadProgress !== null ? { uploadProgress } : {}),
2103
+ alt: (image instanceof HTMLImageElement ? image.alt : "") ||
2104
+ element.textContent?.trim() ||
2105
+ "Image placeholder",
2106
+ };
2107
+ }
2108
+ return {
2109
+ id,
2110
+ type: "paragraph",
2111
+ markdown: editorHtmlToMarkdown(normalizeSoftBreakHtml(element.innerHTML)),
2112
+ };
2113
+ }
2114
+ function readImageUploadProgress(element) {
2115
+ const rawProgress = element.getAttribute("data-image-upload-progress");
2116
+ if (rawProgress === null) {
2117
+ return null;
2118
+ }
2119
+ const progress = Number(rawProgress);
2120
+ return Number.isFinite(progress) ? normalizeUploadProgress(progress) : null;
2121
+ }
2122
+ function readImageCustomWidth(element) {
2123
+ const width = Number(element.getAttribute("data-image-custom-width"));
2124
+ return Number.isFinite(width) ? normalizeImageCustomWidth(width) : null;
2125
+ }
2126
+ function readToolBlockFromElement(element, id) {
2127
+ const title = readOptionalToolAttribute(element, "data-tool-title");
2128
+ const markdown = readDecodedToolAttribute(element, "data-tool-markdown", id);
2129
+ return {
2130
+ id,
2131
+ type: "tool",
2132
+ toolType: element.getAttribute("data-tool-type") ?? "unknown",
2133
+ status: readToolStatusAttribute(element),
2134
+ ...(title ? { title } : {}),
2135
+ ...(markdown ? { markdown } : {}),
2136
+ ...readToolDataAttribute(element, id),
2137
+ };
2138
+ }
2139
+ function readToolStatusAttribute(element) {
2140
+ const status = element.getAttribute("data-tool-status");
2141
+ return status === "pending" || status === "error" ? status : "success";
2142
+ }
2143
+ function readOptionalToolAttribute(element, name) {
2144
+ return element.getAttribute(name)?.trim() || "";
2145
+ }
2146
+ function readDecodedToolAttribute(element, name, blockId) {
2147
+ const value = readOptionalToolAttribute(element, name);
2148
+ if (!value) {
2149
+ return "";
2150
+ }
2151
+ try {
2152
+ return decodeURIComponent(value);
2153
+ }
2154
+ catch (error) {
2155
+ console.warn("[RichTextEditor] Could not decode tool block attribute.", {
2156
+ blockId,
2157
+ error,
2158
+ name,
2159
+ });
2160
+ return "";
2161
+ }
2162
+ }
2163
+ function readToolDataAttribute(element, blockId) {
2164
+ const encodedData = readDecodedToolAttribute(element, "data-tool-data", blockId);
2165
+ if (!encodedData) {
2166
+ return {};
2167
+ }
2168
+ try {
2169
+ return { data: JSON.parse(encodedData) };
2170
+ }
2171
+ catch (error) {
2172
+ console.warn("[RichTextEditor] Could not parse tool block data.", {
2173
+ blockId,
2174
+ error,
2175
+ });
2176
+ return {};
2177
+ }
2178
+ }
2179
+ function updateImageAlignmentButtons(figure, activeAlignment) {
2180
+ figure
2181
+ .querySelectorAll("[data-image-align-action]")
2182
+ .forEach((button) => {
2183
+ if (button.getAttribute("data-image-align-action") === activeAlignment) {
2184
+ button.setAttribute("data-image-align-active", "true");
2185
+ }
2186
+ else {
2187
+ button.removeAttribute("data-image-align-active");
2188
+ }
2189
+ });
2190
+ }
2191
+ function updateImageSizeButtons(figure, activeDisplaySize) {
2192
+ figure
2193
+ .querySelectorAll("[data-image-size-action]")
2194
+ .forEach((button) => {
2195
+ if (button.getAttribute("data-image-size-action") === activeDisplaySize) {
2196
+ button.setAttribute("data-image-size-active", "true");
2197
+ }
2198
+ else {
2199
+ button.removeAttribute("data-image-size-active");
2200
+ }
2201
+ });
2202
+ }
2203
+ function updateImageDisplaySizeAttributes(figure, block) {
2204
+ const { customWidth, displaySize } = getImageDisplayAttributes(block);
2205
+ if (displaySize) {
2206
+ figure.setAttribute("data-image-size", displaySize);
2207
+ }
2208
+ else {
2209
+ figure.removeAttribute("data-image-size");
2210
+ }
2211
+ if (customWidth) {
2212
+ figure.setAttribute("data-image-custom-width", String(customWidth));
2213
+ figure.style.setProperty("--bayon-rte-image-custom-width", `${customWidth}px`);
2214
+ }
2215
+ else {
2216
+ figure.removeAttribute("data-image-custom-width");
2217
+ figure.style.removeProperty("--bayon-rte-image-custom-width");
2218
+ }
2219
+ const input = figure.querySelector("[data-image-custom-width-action]");
2220
+ if (input) {
2221
+ input.value = String(customWidth ?? IMAGE_CUSTOM_WIDTH_FALLBACK);
2222
+ }
2223
+ updateImageSizeButtons(figure, displaySize);
2224
+ }
2225
+ function animateImageAlignmentChange(figure, updateAlignment) {
2226
+ const previousRect = figure.getBoundingClientRect();
2227
+ updateAlignment();
2228
+ const nextRect = figure.getBoundingClientRect();
2229
+ const deltaX = previousRect.left - nextRect.left;
2230
+ const deltaY = previousRect.top - nextRect.top;
2231
+ if (!deltaX && !deltaY) {
2232
+ return;
2233
+ }
2234
+ figure.style.transition = "none";
2235
+ figure.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
2236
+ void figure.offsetWidth;
2237
+ window.requestAnimationFrame(() => {
2238
+ figure.style.transition = "transform 220ms cubic-bezier(0.22, 1, 0.36, 1)";
2239
+ figure.style.transform = "translate(0, 0)";
2240
+ window.setTimeout(() => {
2241
+ figure.style.transition = "";
2242
+ figure.style.transform = "";
2243
+ }, 240);
2244
+ });
2245
+ }
2246
+ function readEditorChildren(element) {
2247
+ const children = readEditorChildBlocks(element);
2248
+ return children.length > 0 ? { children } : {};
2249
+ }
2250
+ function readEditorChildBlocks(element) {
2251
+ const nestedContent = Array.from(element.children).find((child) => {
2252
+ return child.hasAttribute("data-rich-text-nested-content");
2253
+ });
2254
+ if (nestedContent instanceof HTMLElement) {
2255
+ return readEditorNestedBlocks(nestedContent);
2256
+ }
2257
+ const childContainer = Array.from(element.children).find((child) => {
2258
+ return child.hasAttribute("data-rich-text-children");
2259
+ });
2260
+ if (childContainer instanceof HTMLElement) {
2261
+ return readEditorListBlocks(childContainer);
2262
+ }
2263
+ return Array.from(element.children).flatMap((child) => {
2264
+ if (child.hasAttribute("data-rich-text-row") ||
2265
+ child.hasAttribute("data-toggle-content")) {
2266
+ return [];
2267
+ }
2268
+ if (isEditorListContainer(child)) {
2269
+ return readEditorListBlocks(child);
2270
+ }
2271
+ return isReadableEditorBlock(child) ? [readBlockFromElement(child)] : [];
2272
+ });
2273
+ }
2274
+ function readToggleChildBlocks(element) {
2275
+ const content = getDirectToggleContentElement(element);
2276
+ return content
2277
+ ? readEditorNestedBlocks(content)
2278
+ : readEditorChildBlocks(element);
2279
+ }
2280
+ function readEditorNestedBlocks(element) {
2281
+ return Array.from(element.childNodes).flatMap((node) => {
2282
+ if (node.nodeType === Node.TEXT_NODE) {
2283
+ const markdown = editorHtmlToMarkdown(node.textContent ?? "");
2284
+ return markdown
2285
+ ? [
2286
+ {
2287
+ id: createRichTextBlockId(),
2288
+ type: "paragraph",
2289
+ markdown,
2290
+ },
2291
+ ]
2292
+ : [];
2293
+ }
2294
+ if (!(node instanceof Element)) {
2295
+ return [];
2296
+ }
2297
+ if (isEmptyToggleContentPlaceholderBlock(node)) {
2298
+ return [];
2299
+ }
2300
+ if (isEditorListContainer(node)) {
2301
+ return readEditorListBlocks(node);
2302
+ }
2303
+ return isReadableEditorBlock(node) ? [readBlockFromElement(node)] : [];
2304
+ });
2305
+ }
2306
+ function readEditorListBlocks(element) {
2307
+ return Array.from(element.childNodes).flatMap((node) => {
2308
+ return node instanceof Element && isReadableEditorBlock(node)
2309
+ ? [readBlockFromElement(node)]
2310
+ : [];
2311
+ });
2312
+ }
2313
+ function getDirectTreeRowLabel(element, type) {
2314
+ const selector = type === "bullet"
2315
+ ? "[data-bullet-label]"
2316
+ : type === "ordered"
2317
+ ? "[data-ordered-label]"
2318
+ : type === "checkbox"
2319
+ ? "[data-checkbox-label]"
2320
+ : "[data-toggle-label]";
2321
+ return element.querySelector(`:scope > [data-rich-text-row] > ${selector}`);
2322
+ }
2323
+ function isEmptyToggleContentPlaceholderBlock(element) {
2324
+ return (element.hasAttribute("data-toggle-content-placeholder-block") &&
2325
+ editorHtmlToMarkdown(normalizeSoftBreakHtml(element.innerHTML)).trim()
2326
+ .length === 0);
2327
+ }
2328
+ function isEditorListContainer(element) {
2329
+ return ((element.tagName === "UL" || element.tagName === "OL") &&
2330
+ element.hasAttribute("data-rich-text-children"));
2331
+ }
2332
+ export function normalizeSoftBreakHtml(html) {
2333
+ return html
2334
+ .replace(/<span(?:\s+data-checkbox-label(?:=(?:""|'')?)?)?[^>]*>((?:\s*<br\s*\/?>\s*)+)<\/span>/gi, (_match, breaks) => {
2335
+ return breaks.replace(/<br\s*\/?>/gi, "<br>").replace(/\s+/g, "");
2336
+ })
2337
+ .replace(/<br\s*\/?>/gi, "<br>");
2338
+ }
2339
+ function ensureEditorBlockId(element, fallbackId = createRichTextBlockId()) {
2340
+ const currentId = element.getAttribute("data-block-id");
2341
+ if (currentId) {
2342
+ return currentId;
2343
+ }
2344
+ element.setAttribute("data-block-id", fallbackId);
2345
+ return fallbackId;
2346
+ }
2347
+ function isEditorHtmlEmpty(html) {
2348
+ return (html === "" || /^<p data-block-id="[^"]*">(?:&nbsp;)?<\/p>$/.test(html));
2349
+ }
2350
+ function applyActiveTextBlockShortcut(body) {
2351
+ if (!body) {
2352
+ return false;
2353
+ }
2354
+ const selection = window.getSelection();
2355
+ if (!selection || selection.rangeCount === 0) {
2356
+ return false;
2357
+ }
2358
+ const activeBlock = getActiveEditorBlock(selection.getRangeAt(0), body);
2359
+ if (!activeBlock ||
2360
+ activeBlock.hasAttribute("data-rich-text-checkbox") ||
2361
+ !["DIV", "P"].includes(activeBlock.tagName)) {
2362
+ return false;
2363
+ }
2364
+ const shortcut = getTextBlockShortcut(activeBlock.textContent ?? "") ??
2365
+ getTextBlockShortcut(editorHtmlToMarkdown(activeBlock.innerHTML));
2366
+ if (!shortcut) {
2367
+ return false;
2368
+ }
2369
+ const id = activeBlock.getAttribute("data-block-id") || createRichTextBlockId();
2370
+ activeBlock.insertAdjacentHTML("afterend", richTextBlocksToEditorHtml([
2371
+ textBlockShortcutToRichTextBlock(id, shortcut),
2372
+ ]));
2373
+ const replacementBlock = activeBlock.nextElementSibling;
2374
+ activeBlock.remove();
2375
+ if (shortcut.type === "divider") {
2376
+ replacementBlock?.insertAdjacentHTML("afterend", `<p data-block-id="${escapeHtmlAttribute(createRichTextBlockId())}" data-suppress-special-block-tool="true">&nbsp;</p>`);
2377
+ focusBlockStart(replacementBlock?.nextElementSibling ?? null);
2378
+ return shortcut.type;
2379
+ }
2380
+ if (shortcut.type === "checkbox") {
2381
+ focusBlockEnd(replacementBlock?.querySelector("[data-checkbox-label]") ?? null);
2382
+ return shortcut.type;
2383
+ }
2384
+ if (shortcut.type === "bullet" || shortcut.type === "ordered") {
2385
+ focusBlockEnd(replacementBlock?.querySelector(shortcut.type === "ordered"
2386
+ ? "[data-ordered-label]"
2387
+ : "[data-bullet-label]") ?? null);
2388
+ return shortcut.type;
2389
+ }
2390
+ if (shortcut.type === "toggle") {
2391
+ focusBlockStart(replacementBlock?.querySelector("[data-toggle-label]") ?? null);
2392
+ return shortcut.type;
2393
+ }
2394
+ focusBlockEnd(replacementBlock);
2395
+ return shortcut.type;
2396
+ }
2397
+ function getHistoryBlockTypeForElement(element) {
2398
+ if (!element) {
2399
+ return "paragraph";
2400
+ }
2401
+ if (element.hasAttribute("data-rich-text-checkbox")) {
2402
+ return "checkbox";
2403
+ }
2404
+ if (element.hasAttribute("data-rich-text-bullet")) {
2405
+ return "bullet";
2406
+ }
2407
+ if (element.hasAttribute("data-rich-text-ordered")) {
2408
+ return "ordered";
2409
+ }
2410
+ if (element.hasAttribute("data-rich-text-toggle")) {
2411
+ return "toggle";
2412
+ }
2413
+ if (element.hasAttribute("data-rich-text-code")) {
2414
+ return "code";
2415
+ }
2416
+ if (element.hasAttribute("data-rich-text-heading")) {
2417
+ return "heading";
2418
+ }
2419
+ return "paragraph";
2420
+ }
2421
+ function getSpecialBlockActionBlockType(action) {
2422
+ if (action.label === "Divider") {
2423
+ return "divider";
2424
+ }
2425
+ if (action.label === "Code block" || action.label === "Embedded HTML") {
2426
+ return "code";
2427
+ }
2428
+ if (action.label === "Title") {
2429
+ return "heading";
2430
+ }
2431
+ if (action.label === "Bullet list") {
2432
+ return "bullet";
2433
+ }
2434
+ if (action.label === "Toggle") {
2435
+ return "toggle";
2436
+ }
2437
+ if (action.label === "Quote") {
2438
+ return "quote";
2439
+ }
2440
+ return "paragraph";
2441
+ }
2442
+ function normalizeActiveTextBlockTypography(body) {
2443
+ if (!body) {
2444
+ return;
2445
+ }
2446
+ const selection = window.getSelection();
2447
+ if (!selection || selection.rangeCount === 0) {
2448
+ return;
2449
+ }
2450
+ const range = selection.getRangeAt(0);
2451
+ const activeBlock = getActiveEditorBlock(range, body);
2452
+ if (!activeBlock ||
2453
+ activeBlock.tagName === "PRE" ||
2454
+ isNestableEditorBlock(activeBlock)) {
2455
+ return;
2456
+ }
2457
+ const currentMarkdown = editorHtmlToMarkdown(activeBlock.innerHTML);
2458
+ if (shouldDelayTypographyNormalization(activeBlock.textContent ?? "")) {
2459
+ return;
2460
+ }
2461
+ const nextHtml = textBlockMarkdownToEditorHtml(currentMarkdown);
2462
+ if (activeBlock.innerHTML === nextHtml) {
2463
+ return;
2464
+ }
2465
+ const cursorOffset = getTextOffsetWithin(activeBlock, range);
2466
+ const normalizedCursorOffset = editorHtmlToMarkdown(activeBlock.textContent?.slice(0, cursorOffset) ?? "").length;
2467
+ activeBlock.innerHTML = nextHtml;
2468
+ focusBlockAtTextOffset(activeBlock, normalizedCursorOffset);
2469
+ }
2470
+ function focusBlockEnd(block) {
2471
+ if (!block) {
2472
+ return;
2473
+ }
2474
+ const range = document.createRange();
2475
+ range.selectNodeContents(block);
2476
+ range.collapse(false);
2477
+ const selection = window.getSelection();
2478
+ selection?.removeAllRanges();
2479
+ selection?.addRange(range);
2480
+ }
2481
+ function focusBlockAtTextOffset(block, offset) {
2482
+ const target = findFocusTargetAtVisibleTextOffset(block, offset);
2483
+ if (target) {
2484
+ const range = document.createRange();
2485
+ range.setStart(target.node, target.offset);
2486
+ range.collapse(true);
2487
+ const selection = window.getSelection();
2488
+ selection?.removeAllRanges();
2489
+ selection?.addRange(range);
2490
+ return;
2491
+ }
2492
+ focusBlockEnd(block);
2493
+ }
2494
+ function getTextOffsetWithin(root, range) {
2495
+ const textRange = document.createRange();
2496
+ textRange.selectNodeContents(root);
2497
+ textRange.setEnd(range.startContainer, range.startOffset);
2498
+ return getEditorVisibleTextLength(textRange.cloneContents());
2499
+ }
2500
+ function getTextAfterRange(root, range) {
2501
+ const textRange = document.createRange();
2502
+ textRange.selectNodeContents(root);
2503
+ textRange.setStart(range.startContainer, range.startOffset);
2504
+ return textRange.toString();
2505
+ }
2506
+ function getBulletTextOffsetWithin(block, label, range) {
2507
+ if (label) {
2508
+ return getTextOffsetWithin(label, range);
2509
+ }
2510
+ return Math.max(0, getTextOffsetWithin(block, range) - getBulletMarkerLength(block));
2511
+ }
2512
+ function getBulletMarkdownFromElement(element) {
2513
+ const label = getDirectTreeRowLabel(element, "bullet");
2514
+ if (label) {
2515
+ return editorHtmlToMarkdown(normalizeSoftBreakHtml(label.innerHTML));
2516
+ }
2517
+ return editorHtmlToMarkdown(getBulletFallbackText(element));
2518
+ }
2519
+ function getOrderedMarkdownFromElement(element) {
2520
+ const label = getDirectTreeRowLabel(element, "ordered");
2521
+ if (label) {
2522
+ return editorHtmlToMarkdown(normalizeSoftBreakHtml(label.innerHTML));
2523
+ }
2524
+ return editorHtmlToMarkdown(getBulletFallbackText(element));
2525
+ }
2526
+ function setBulletBlockMarkdown(element, markdown) {
2527
+ element.innerHTML = `<div data-rich-text-row=""><span data-bullet-marker="" contenteditable="false" aria-hidden="true"></span><span data-bullet-label="" data-placeholder="List item">${textBlockMarkdownToEditorHtmlWithPlaceholder(markdown)}</span></div>${treeRowChildrenToEditorHtml(readEditorChildBlocks(element))}`;
2528
+ }
2529
+ function getBulletFallbackText(element) {
2530
+ const clone = element.cloneNode(true);
2531
+ clone.querySelector("[data-rich-text-children]")?.remove();
2532
+ return (clone.textContent ?? "").replace(/^\s*(?:•|\u2022)\s*/, "");
2533
+ }
2534
+ function getBulletMarkerLength(element) {
2535
+ const text = element.textContent ?? "";
2536
+ const match = text.match(/^\s*(?:•|\u2022)\s*/);
2537
+ return match?.[0].length ?? 0;
2538
+ }
2539
+ export function hasEditableTrailingWhitespace(value) {
2540
+ return /[\s\u00a0]$/.test(value);
2541
+ }
2542
+ export function shouldDelayTypographyNormalization(value) {
2543
+ return hasEditableTrailingWhitespace(value) && !hasDashSequence(value);
2544
+ }
2545
+ export function splitTreeRowMarkdownAtTextOffset(markdown, offset) {
2546
+ const visibleOffset = Math.trunc(offset);
2547
+ const visibleTextLength = getVisibleTextLength(getMarkdownVisibleText(markdown));
2548
+ if (visibleOffset <= 0) {
2549
+ return {
2550
+ after: normalizeSplitMarkdownFragment(markdown),
2551
+ before: "",
2552
+ };
2553
+ }
2554
+ if (visibleOffset >= visibleTextLength) {
2555
+ return {
2556
+ after: "",
2557
+ before: normalizeSplitMarkdownFragment(markdown),
2558
+ };
2559
+ }
2560
+ const splitOffset = clamp(getMarkdownIndexAtVisibleTextOffset(markdown, visibleOffset), 0, markdown.length);
2561
+ return {
2562
+ after: normalizeSplitMarkdownFragment(markdown.slice(splitOffset)),
2563
+ before: normalizeSplitMarkdownFragment(markdown.slice(0, splitOffset)),
2564
+ };
2565
+ }
2566
+ function normalizeSplitMarkdownFragment(markdown) {
2567
+ return markdown.trim().length > 0 ? markdown.trimStart() : "";
2568
+ }
2569
+ export const splitCheckboxMarkdownAtTextOffset = splitTreeRowMarkdownAtTextOffset;
2570
+ function getMarkdownIndexAtVisibleTextOffset(markdown, offset) {
2571
+ let visibleOffset = 0;
2572
+ for (let index = 0; index < markdown.length;) {
2573
+ if (isSoftBreakMarkdownAt(markdown, index)) {
2574
+ visibleOffset += 1;
2575
+ if (visibleOffset >= offset) {
2576
+ return index + 3;
2577
+ }
2578
+ index += 3;
2579
+ continue;
2580
+ }
2581
+ const nextIndex = getNextUtf16Index(markdown, index);
2582
+ if (isMarkdownSyntaxAt(markdown, index)) {
2583
+ index = nextIndex;
2584
+ continue;
2585
+ }
2586
+ visibleOffset += 1;
2587
+ if (visibleOffset >= offset) {
2588
+ return nextIndex;
2589
+ }
2590
+ index = nextIndex;
2591
+ }
2592
+ return markdown.length;
2593
+ }
2594
+ function findFocusTargetAtVisibleTextOffset(root, offset) {
2595
+ const remaining = { value: Math.max(0, Math.trunc(offset)) };
2596
+ return findFocusTargetInChildren(root, remaining);
2597
+ }
2598
+ function findFocusTargetInChildren(root, remaining) {
2599
+ for (const child of Array.from(root.childNodes)) {
2600
+ if (child.nodeType === Node.TEXT_NODE) {
2601
+ const text = child.textContent ?? "";
2602
+ const textLength = getVisibleTextLength(text);
2603
+ if (remaining.value <= textLength) {
2604
+ return {
2605
+ node: child,
2606
+ offset: getUtf16IndexAtVisibleTextOffset(text, remaining.value),
2607
+ };
2608
+ }
2609
+ remaining.value -= textLength;
2610
+ continue;
2611
+ }
2612
+ if (isLineBreakNode(child)) {
2613
+ const parent = child.parentNode;
2614
+ if (!parent) {
2615
+ continue;
2616
+ }
2617
+ if (remaining.value === 0) {
2618
+ return { node: parent, offset: getChildNodeIndex(child) };
2619
+ }
2620
+ if (remaining.value === 1) {
2621
+ return { node: parent, offset: getChildNodeIndex(child) + 1 };
2622
+ }
2623
+ remaining.value -= 1;
2624
+ continue;
2625
+ }
2626
+ const target = findFocusTargetInChildren(child, remaining);
2627
+ if (target) {
2628
+ return target;
2629
+ }
2630
+ }
2631
+ return null;
2632
+ }
2633
+ function getChildNodeIndex(node) {
2634
+ return Array.prototype.indexOf.call(node.parentNode?.childNodes ?? [], node);
2635
+ }
2636
+ function getEditorVisibleTextLength(root) {
2637
+ let length = 0;
2638
+ for (const child of Array.from(root.childNodes)) {
2639
+ if (child.nodeType === Node.TEXT_NODE) {
2640
+ length += getVisibleTextLength(child.textContent ?? "");
2641
+ continue;
2642
+ }
2643
+ if (isLineBreakNode(child)) {
2644
+ length += 1;
2645
+ continue;
2646
+ }
2647
+ length += getEditorVisibleTextLength(child);
2648
+ }
2649
+ return length;
2650
+ }
2651
+ function isLineBreakNode(node) {
2652
+ return (node.nodeType === Node.ELEMENT_NODE && node.tagName === "BR");
2653
+ }
2654
+ function getVisibleTextLength(value) {
2655
+ return Array.from(value).length;
2656
+ }
2657
+ function getUtf16IndexAtVisibleTextOffset(value, offset) {
2658
+ if (offset <= 0) {
2659
+ return 0;
2660
+ }
2661
+ let visibleOffset = 0;
2662
+ for (let index = 0; index < value.length;) {
2663
+ const nextIndex = getNextUtf16Index(value, index);
2664
+ visibleOffset += 1;
2665
+ if (visibleOffset >= offset) {
2666
+ return nextIndex;
2667
+ }
2668
+ index = nextIndex;
2669
+ }
2670
+ return value.length;
2671
+ }
2672
+ function getNextUtf16Index(value, index) {
2673
+ const codePoint = value.codePointAt(index);
2674
+ return index + (codePoint && codePoint > 0xffff ? 2 : 1);
2675
+ }
2676
+ function getMarkdownVisibleText(markdown) {
2677
+ return markdown
2678
+ .replace(/ {2}\n/g, "\n")
2679
+ .replace(/`([^`]*)`/g, "$1")
2680
+ .replace(/\*\*([^*]*)\*\*/g, "$1")
2681
+ .replace(/_([^_]*)_/g, "$1")
2682
+ .replace(/\[([^\]]*)\]\([^)]+\)/g, "$1");
2683
+ }
2684
+ function isSoftBreakMarkdownAt(markdown, index) {
2685
+ return markdown.slice(index, index + 3) === " \n";
2686
+ }
2687
+ function isMarkdownSyntaxAt(markdown, index) {
2688
+ return (isStrongDelimiterAt(markdown, index) || isInlineSyntaxAt(markdown, index));
2689
+ }
2690
+ function isStrongDelimiterAt(markdown, index) {
2691
+ return (markdown[index] === "*" &&
2692
+ (markdown[index - 1] === "*" || markdown[index + 1] === "*"));
2693
+ }
2694
+ function isInlineSyntaxAt(markdown, index) {
2695
+ const char = markdown[index];
2696
+ if (isInsideMarkdownLinkTarget(markdown, index)) {
2697
+ return true;
2698
+ }
2699
+ if (char === "`" || char === "_") {
2700
+ return true;
2701
+ }
2702
+ if (char === "[" || char === "]") {
2703
+ return true;
2704
+ }
2705
+ return false;
2706
+ }
2707
+ function isInsideMarkdownLinkTarget(markdown, index) {
2708
+ const linkOpenIndex = markdown.lastIndexOf("](", index);
2709
+ if (linkOpenIndex === -1) {
2710
+ return false;
2711
+ }
2712
+ const linkCloseIndex = markdown.indexOf(")", linkOpenIndex + 2);
2713
+ return linkCloseIndex !== -1 && index <= linkCloseIndex;
2714
+ }
2715
+ export function getCheckboxEnterAction(event) {
2716
+ return event.shiftKey ? "line-break" : "next-checkbox";
2717
+ }
2718
+ export function shouldPadTrailingLineBreak(textAfterCursor) {
2719
+ return textAfterCursor.length === 0;
2720
+ }
2721
+ export function resolveTranscriptionConfig(config = {}) {
2722
+ return {
2723
+ enabled: config.enabled ?? true,
2724
+ language: config.language?.trim() || undefined,
2725
+ };
2726
+ }
2727
+ export function appendTranscriptText(currentText, transcript) {
2728
+ const nextText = transcript.trim();
2729
+ if (!nextText) {
2730
+ return currentText;
2731
+ }
2732
+ if (!currentText) {
2733
+ return nextText;
2734
+ }
2735
+ return `${currentText}${hasEditableTrailingWhitespace(currentText) ? "" : " "}${nextText}`;
2736
+ }
2737
+ export function shouldRefreshEditorBodyHtml(body, editableHtml, nextBlocks) {
2738
+ if (body.innerHTML === editableHtml) {
2739
+ return false;
2740
+ }
2741
+ return (canonicalSerialize(readBlocksFromEditor(body)) !==
2742
+ canonicalSerialize(sanitizeRichTextBlocks(nextBlocks)));
2743
+ }
2744
+ export function restoreScrollPositionAfter(scrollTarget, callback) {
2745
+ const scrollX = scrollTarget.scrollX;
2746
+ const scrollY = scrollTarget.scrollY;
2747
+ callback();
2748
+ scrollTarget.scrollTo(scrollX, scrollY);
2749
+ scrollTarget.requestAnimationFrame?.(() => {
2750
+ scrollTarget.scrollTo(scrollX, scrollY);
2751
+ });
2752
+ }
2753
+ function hasDashSequence(value) {
2754
+ return /--|––|–-|---|—-/.test(value);
2755
+ }
2756
+ function insertLineBreakAtRange(range, options = {}) {
2757
+ const breakElement = document.createElement("br");
2758
+ range.insertNode(breakElement);
2759
+ if (options.padTrailingBreak) {
2760
+ const paddingBreakElement = document.createElement("br");
2761
+ breakElement.parentNode?.insertBefore(paddingBreakElement, breakElement.nextSibling);
2762
+ }
2763
+ range.setStartAfter(breakElement);
2764
+ range.collapse(true);
2765
+ const selection = window.getSelection();
2766
+ selection?.removeAllRanges();
2767
+ selection?.addRange(range);
2768
+ }
2769
+ function checkboxBlockToEditorHtml(block) {
2770
+ const blockId = escapeHtmlAttribute(block.id);
2771
+ const checked = block.checked ? " checked" : "";
2772
+ const children = treeRowChildrenToEditorHtml(block.children ?? []);
2773
+ return `<li data-block-id="${blockId}" data-rich-text-checkbox=""><div data-rich-text-row=""><input contenteditable="false" type="checkbox"${checked}><span data-checkbox-label="">${textBlockMarkdownToEditorHtmlWithPlaceholder(block.markdown)}</span></div>${children}</li>`;
2774
+ }
2775
+ function listBlockToEditorHtml(block) {
2776
+ const blockId = escapeHtmlAttribute(block.id);
2777
+ const children = treeRowChildrenToEditorHtml(block.children ?? []);
2778
+ return `<li data-block-id="${blockId}" data-rich-text-bullet=""><div data-rich-text-row=""><span data-bullet-marker="" contenteditable="false" aria-hidden="true"></span><span data-bullet-label="" data-placeholder="List item">${textBlockMarkdownToEditorHtmlWithPlaceholder(block.markdown)}</span></div>${children}</li>`;
2779
+ }
2780
+ function orderedBlockToEditorHtml(block) {
2781
+ const blockId = escapeHtmlAttribute(block.id);
2782
+ const children = treeRowChildrenToEditorHtml(block.children ?? []);
2783
+ return `<li data-block-id="${blockId}" data-rich-text-ordered=""><div data-rich-text-row=""><span data-ordered-marker="" contenteditable="false" aria-hidden="true"></span><span data-ordered-label="" data-placeholder="Numbered item">${textBlockMarkdownToEditorHtmlWithPlaceholder(block.markdown)}</span></div>${children}</li>`;
2784
+ }
2785
+ function toggleBlockToEditorHtml(block) {
2786
+ const blockId = escapeHtmlAttribute(block.id);
2787
+ const collapsed = block.collapsed ? "true" : "false";
2788
+ const content = toggleContentToEditorHtml(block.children);
2789
+ return `<li data-block-id="${blockId}" data-rich-text-toggle="" data-toggle-collapsed="${collapsed}">${toggleTitleRowToEditorHtml(block)}${content}</li>`;
2790
+ }
2791
+ function treeRowBlockToEditorHtml(block) {
2792
+ if (block.type === "checkbox") {
2793
+ return checkboxBlockToEditorHtml({
2794
+ checked: block.checked,
2795
+ id: block.id,
2796
+ markdown: block.markdown,
2797
+ type: "checkbox",
2798
+ });
2799
+ }
2800
+ if (block.type === "ordered") {
2801
+ return orderedBlockToEditorHtml({
2802
+ id: block.id,
2803
+ markdown: block.markdown,
2804
+ type: "ordered",
2805
+ });
2806
+ }
2807
+ if (block.type === "toggle") {
2808
+ return toggleBlockToEditorHtml({
2809
+ children: [],
2810
+ collapsed: block.collapsed,
2811
+ id: block.id,
2812
+ markdown: block.markdown,
2813
+ type: "toggle",
2814
+ });
2815
+ }
2816
+ return listBlockToEditorHtml({
2817
+ id: block.id,
2818
+ markdown: block.markdown,
2819
+ type: "bullet",
2820
+ });
2821
+ }
2822
+ function treeRowChildrenToEditorHtml(blocks) {
2823
+ return blocks.length > 0
2824
+ ? `<div data-rich-text-nested-content="">${richTextBlocksToEditorHtml(blocks)}</div>`
2825
+ : "";
2826
+ }
2827
+ function toggleTitleRowToEditorHtml(block) {
2828
+ const actionLabel = block.collapsed ? "Expand toggle" : "Collapse toggle";
2829
+ return `<div data-rich-text-row=""><button aria-label="${actionLabel}" class="bayon-rte-toggle-button" contenteditable="false" data-toggle-collapse="" title="${actionLabel}" type="button"><span class="bayon-rte-toggle-caret" aria-hidden="true"></span></button><span data-toggle-label="" data-placeholder="Toggle title">${textBlockMarkdownToEditorHtmlWithPlaceholder(block.markdown)}</span></div>`;
2830
+ }
2831
+ function toggleContentToEditorHtml(blocks) {
2832
+ const content = blocks.length > 0
2833
+ ? richTextBlocksToEditorHtml(blocks)
2834
+ : toggleContentPlaceholderBlockToEditorHtml();
2835
+ return `<div data-toggle-content="" data-placeholder="Toggle content">${content}</div>`;
2836
+ }
2837
+ function toggleContentPlaceholderBlockToEditorHtml() {
2838
+ return `<p data-block-id="${escapeHtmlAttribute(createRichTextBlockId())}" data-toggle-content-placeholder-block="" data-placeholder="Toggle content"></p>`;
2839
+ }
2840
+ export function getSelectionToolbarPosition({ bodyClientWidth, bodyRect, rangeRect, scrollTop, }) {
2841
+ const selectionTop = rangeRect.top - bodyRect.top + scrollTop;
2842
+ const selectionBottom = rangeRect.bottom - bodyRect.top + scrollTop;
2843
+ const spaceAbove = rangeRect.top - bodyRect.top;
2844
+ const spaceBelow = bodyRect.bottom - rangeRect.bottom;
2845
+ const toolbarOffset = SELECTION_TOOLBAR_HEIGHT + SELECTION_TOOLBAR_SELECTION_GAP;
2846
+ const placement = spaceAbove >= toolbarOffset || spaceAbove >= spaceBelow ? "above" : "below";
2847
+ return {
2848
+ left: clamp(rangeRect.left - bodyRect.left + rangeRect.width / 2, SELECTION_TOOLBAR_HALF_WIDTH, Math.max(SELECTION_TOOLBAR_HALF_WIDTH, bodyClientWidth - SELECTION_TOOLBAR_HALF_WIDTH)),
2849
+ placement,
2850
+ top: placement === "above"
2851
+ ? Math.max(0, selectionTop - toolbarOffset)
2852
+ : selectionBottom + SELECTION_TOOLBAR_SELECTION_GAP,
2853
+ };
2854
+ }
2855
+ function getDirectToggleContentElement(element) {
2856
+ return (Array.from(element.children).find((child) => {
2857
+ return child.hasAttribute("data-toggle-content");
2858
+ }) ?? null);
2859
+ }
2860
+ function treeEditorBlocksToHtml(blocks, treeKind) {
2861
+ const tagName = treeKind === "ordered" ? "ol" : "ul";
2862
+ return `<${tagName} data-rich-text-children="">${blocks
2863
+ .map(richTextBlockToEditorHtml)
2864
+ .join("")}</${tagName}>`;
2865
+ }
2866
+ function isTreeEditorBlock(block) {
2867
+ return (block?.type === "bullet" ||
2868
+ block?.type === "ordered" ||
2869
+ block?.type === "checkbox" ||
2870
+ block?.type === "toggle");
2871
+ }
2872
+ function getTreeListKind(block) {
2873
+ return block?.type === "ordered" ? "ordered" : "unordered";
2874
+ }
2875
+ function textBlockShortcutToRichTextBlock(id, shortcut) {
2876
+ if (shortcut.type === "divider") {
2877
+ return { id, type: "divider" };
2878
+ }
2879
+ if (shortcut.type === "code") {
2880
+ return { id, type: "code", text: shortcut.text };
2881
+ }
2882
+ if (shortcut.type === "checkbox") {
2883
+ return {
2884
+ id,
2885
+ type: "checkbox",
2886
+ checked: shortcut.checked,
2887
+ markdown: shortcut.markdown,
2888
+ };
2889
+ }
2890
+ if (shortcut.type === "toggle") {
2891
+ return {
2892
+ id,
2893
+ type: "toggle",
2894
+ collapsed: shortcut.collapsed,
2895
+ markdown: shortcut.markdown,
2896
+ children: [],
2897
+ };
2898
+ }
2899
+ if (shortcut.type === "ordered") {
2900
+ return { id, type: "ordered", markdown: shortcut.markdown };
2901
+ }
2902
+ return { id, type: shortcut.type, markdown: shortcut.markdown };
2903
+ }
2904
+ function escapeHtmlText(value) {
2905
+ return value
2906
+ .replace(/&/g, "&amp;")
2907
+ .replace(/</g, "&lt;")
2908
+ .replace(/>/g, "&gt;");
2909
+ }
2910
+ function escapeHtmlAttribute(value) {
2911
+ return escapeHtmlText(value).replace(/"/g, "&quot;");
2912
+ }
2913
+ function cssEscape(value) {
2914
+ if (typeof CSS !== "undefined" && CSS.escape) {
2915
+ return CSS.escape(value);
2916
+ }
2917
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2918
+ }
2919
+ function getSafeImageUrl(value) {
2920
+ const trimmed = value.trim();
2921
+ if (!trimmed || /^javascript:/i.test(trimmed)) {
2922
+ return "";
2923
+ }
2924
+ return trimmed;
2925
+ }