@bendyline/squisq-editor-react 1.6.1 → 2.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.
- package/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +312 -6
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +193 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/mediaReferences.ts +299 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -270
- package/src/diagram/useDiagramData.ts +0 -126
package/src/OutlinePanel.tsx
CHANGED
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* lives in `useHeadingLayout`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type CSSProperties,
|
|
13
|
+
type DragEvent as ReactDragEvent,
|
|
14
|
+
useCallback,
|
|
15
|
+
useEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react';
|
|
12
20
|
import type { Block } from '@bendyline/squisq/schemas';
|
|
13
21
|
import { flattenBlocks, hasTemplate } from '@bendyline/squisq/doc';
|
|
14
22
|
import { extractPlainText } from '@bendyline/squisq/markdown';
|
|
@@ -16,6 +24,7 @@ import { useEditorContext } from './EditorContext';
|
|
|
16
24
|
import { templateLabel } from './TemplatePicker';
|
|
17
25
|
import { useHeadingLayout } from './useHeadingLayout';
|
|
18
26
|
import { usePreviewSettingsOptional } from './PreviewControls';
|
|
27
|
+
import { moveHeadingSectionInSource, type OutlineDropPlacement } from './outlineSource';
|
|
19
28
|
|
|
20
29
|
/**
|
|
21
30
|
* Responsive default width for the outline pane, used when no fixed `width`
|
|
@@ -38,13 +47,34 @@ export interface OutlinePanelProps {
|
|
|
38
47
|
width?: number;
|
|
39
48
|
/** Optional CSS class for the outer container. */
|
|
40
49
|
className?: string;
|
|
50
|
+
/** Disable heading-level changes and section reordering. */
|
|
51
|
+
readOnly?: boolean;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
interface OutlineSectionRef {
|
|
55
|
+
blockId: string;
|
|
56
|
+
headingLine: number;
|
|
57
|
+
depth: number;
|
|
58
|
+
parentId: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ActiveOutlineDrag extends OutlineSectionRef {
|
|
62
|
+
sourceAtStart: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface OutlineDropTarget {
|
|
66
|
+
blockId: string;
|
|
67
|
+
placement: OutlineDropPlacement;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const OUTLINE_DRAG_MIME = 'application/x-squisq-outline-section';
|
|
71
|
+
|
|
72
|
+
export function OutlinePanel({ width, className, readOnly = false }: OutlinePanelProps) {
|
|
44
73
|
const {
|
|
45
74
|
doc,
|
|
46
75
|
markdownSource,
|
|
47
76
|
setMarkdownSource,
|
|
77
|
+
isParsing,
|
|
48
78
|
layoutMode,
|
|
49
79
|
goToBlockByLine,
|
|
50
80
|
activeBlockStartLine,
|
|
@@ -52,6 +82,9 @@ export function OutlinePanel({ width, className }: OutlinePanelProps) {
|
|
|
52
82
|
const paneRef = useRef<HTMLElement | null>(null);
|
|
53
83
|
const { scrollToBlock } = useHeadingLayout(paneRef);
|
|
54
84
|
const cursorActiveId = useActiveOutlineBlockId();
|
|
85
|
+
const activeDragRef = useRef<ActiveOutlineDrag | null>(null);
|
|
86
|
+
const [draggedBlockId, setDraggedBlockId] = useState<string | null>(null);
|
|
87
|
+
const [dropTarget, setDropTarget] = useState<OutlineDropTarget | null>(null);
|
|
55
88
|
|
|
56
89
|
// In block-at-a-time mode there's no live cursor across the whole document
|
|
57
90
|
// (only the active block is mounted), so the highlight follows the card:
|
|
@@ -87,12 +120,94 @@ export function OutlinePanel({ width, className }: OutlinePanelProps) {
|
|
|
87
120
|
// Both editor surfaces resync from `markdownSource` automatically.
|
|
88
121
|
const changeHeadingLevel = useCallback(
|
|
89
122
|
(block: Block, delta: number) => {
|
|
123
|
+
if (readOnly || isParsing) return;
|
|
90
124
|
const line = block.sourceHeading?.position?.start.line;
|
|
91
125
|
if (typeof line !== 'number') return;
|
|
92
126
|
const next = bumpHeadingLevelInSource(markdownSource, line, delta);
|
|
93
127
|
if (next != null) setMarkdownSource(next);
|
|
94
128
|
},
|
|
95
|
-
[markdownSource, setMarkdownSource],
|
|
129
|
+
[isParsing, markdownSource, readOnly, setMarkdownSource],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const clearDragState = useCallback(() => {
|
|
133
|
+
activeDragRef.current = null;
|
|
134
|
+
setDraggedBlockId(null);
|
|
135
|
+
setDropTarget(null);
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const handleDragStart = useCallback(
|
|
139
|
+
(event: ReactDragEvent<HTMLElement>, section: OutlineSectionRef) => {
|
|
140
|
+
if (readOnly || isParsing) {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
activeDragRef.current = { ...section, sourceAtStart: markdownSource };
|
|
146
|
+
setDraggedBlockId(section.blockId);
|
|
147
|
+
setDropTarget(null);
|
|
148
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
149
|
+
// Firefox requires a text payload before it will initiate a native drag.
|
|
150
|
+
// The custom type makes the payload identifiable to other Squisq panes;
|
|
151
|
+
// the in-memory ref remains authoritative for this mounted panel.
|
|
152
|
+
event.dataTransfer.setData(OUTLINE_DRAG_MIME, String(section.headingLine));
|
|
153
|
+
event.dataTransfer.setData('text/plain', String(section.headingLine));
|
|
154
|
+
},
|
|
155
|
+
[isParsing, markdownSource, readOnly],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const handleDragOver = useCallback(
|
|
159
|
+
(event: ReactDragEvent<HTMLElement>, section: OutlineSectionRef) => {
|
|
160
|
+
const dragged = activeDragRef.current;
|
|
161
|
+
if (!dragged) return;
|
|
162
|
+
|
|
163
|
+
// Prevent the editor shell's file-drop handler from changing this to a
|
|
164
|
+
// copy operation while an outline section is being moved.
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
|
|
168
|
+
if (!canDropOnSection(dragged, section) || readOnly || isParsing) {
|
|
169
|
+
event.dataTransfer.dropEffect = 'none';
|
|
170
|
+
setDropTarget(null);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const placement = placementForPointer(event);
|
|
175
|
+
event.dataTransfer.dropEffect = 'move';
|
|
176
|
+
setDropTarget((current) =>
|
|
177
|
+
current?.blockId === section.blockId && current.placement === placement
|
|
178
|
+
? current
|
|
179
|
+
: { blockId: section.blockId, placement },
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
[isParsing, readOnly],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const handleDrop = useCallback(
|
|
186
|
+
(event: ReactDragEvent<HTMLElement>, section: OutlineSectionRef) => {
|
|
187
|
+
const dragged = activeDragRef.current;
|
|
188
|
+
if (!dragged) return;
|
|
189
|
+
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
const placement = placementForPointer(event);
|
|
193
|
+
const canMove =
|
|
194
|
+
!readOnly &&
|
|
195
|
+
!isParsing &&
|
|
196
|
+
dragged.sourceAtStart === markdownSource &&
|
|
197
|
+
canDropOnSection(dragged, section);
|
|
198
|
+
const next = canMove
|
|
199
|
+
? moveHeadingSectionInSource(
|
|
200
|
+
markdownSource,
|
|
201
|
+
dragged.headingLine,
|
|
202
|
+
section.headingLine,
|
|
203
|
+
placement,
|
|
204
|
+
)
|
|
205
|
+
: null;
|
|
206
|
+
|
|
207
|
+
clearDragState();
|
|
208
|
+
if (next != null) setMarkdownSource(next);
|
|
209
|
+
},
|
|
210
|
+
[clearDragState, isParsing, markdownSource, readOnly, setMarkdownSource],
|
|
96
211
|
);
|
|
97
212
|
|
|
98
213
|
// Inherit the active document theme's primary color so the current-row
|
|
@@ -137,9 +252,17 @@ export function OutlinePanel({ width, className }: OutlinePanelProps) {
|
|
|
137
252
|
<OutlineNode
|
|
138
253
|
key={b.id}
|
|
139
254
|
block={b}
|
|
255
|
+
parentId={null}
|
|
140
256
|
activeBlockId={activeBlockId}
|
|
257
|
+
draggedBlockId={draggedBlockId}
|
|
258
|
+
dropTarget={dropTarget}
|
|
259
|
+
mutationsDisabled={readOnly || isParsing}
|
|
141
260
|
onSelect={handleSelect}
|
|
142
261
|
onChangeLevel={changeHeadingLevel}
|
|
262
|
+
onDragStart={handleDragStart}
|
|
263
|
+
onDragOver={handleDragOver}
|
|
264
|
+
onDrop={handleDrop}
|
|
265
|
+
onDragEnd={clearDragState}
|
|
143
266
|
/>
|
|
144
267
|
))}
|
|
145
268
|
</ul>
|
|
@@ -152,17 +275,34 @@ export function OutlinePanel({ width, className }: OutlinePanelProps) {
|
|
|
152
275
|
|
|
153
276
|
function OutlineNode({
|
|
154
277
|
block,
|
|
278
|
+
parentId,
|
|
155
279
|
activeBlockId,
|
|
280
|
+
draggedBlockId,
|
|
281
|
+
dropTarget,
|
|
282
|
+
mutationsDisabled,
|
|
156
283
|
onSelect,
|
|
157
284
|
onChangeLevel,
|
|
285
|
+
onDragStart,
|
|
286
|
+
onDragOver,
|
|
287
|
+
onDrop,
|
|
288
|
+
onDragEnd,
|
|
158
289
|
}: {
|
|
159
290
|
block: Block;
|
|
291
|
+
parentId: string | null;
|
|
160
292
|
activeBlockId: string | null;
|
|
293
|
+
draggedBlockId: string | null;
|
|
294
|
+
dropTarget: OutlineDropTarget | null;
|
|
295
|
+
mutationsDisabled: boolean;
|
|
161
296
|
onSelect: (b: Block) => void;
|
|
162
297
|
onChangeLevel: (block: Block, delta: number) => void;
|
|
298
|
+
onDragStart: (event: ReactDragEvent<HTMLElement>, section: OutlineSectionRef) => void;
|
|
299
|
+
onDragOver: (event: ReactDragEvent<HTMLElement>, section: OutlineSectionRef) => void;
|
|
300
|
+
onDrop: (event: ReactDragEvent<HTMLElement>, section: OutlineSectionRef) => void;
|
|
301
|
+
onDragEnd: () => void;
|
|
163
302
|
}) {
|
|
164
303
|
const heading = block.sourceHeading;
|
|
165
304
|
const depth = heading?.depth ?? 1;
|
|
305
|
+
const headingLine = heading?.position?.start.line;
|
|
166
306
|
const text = heading ? extractPlainText(heading).trim() : '';
|
|
167
307
|
const annotation = heading?.templateAnnotation;
|
|
168
308
|
const tplName = annotation?.template;
|
|
@@ -170,18 +310,52 @@ function OutlineNode({
|
|
|
170
310
|
const isActive = block.id === activeBlockId;
|
|
171
311
|
const canPromote = !!heading && depth > 1;
|
|
172
312
|
const canDemote = !!heading && depth < 6;
|
|
313
|
+
const section =
|
|
314
|
+
heading && typeof headingLine === 'number'
|
|
315
|
+
? { blockId: block.id, headingLine, depth, parentId }
|
|
316
|
+
: null;
|
|
317
|
+
const canDrag = section != null && !mutationsDisabled;
|
|
318
|
+
const isDragging = block.id === draggedBlockId;
|
|
319
|
+
const dropPlacement = dropTarget?.blockId === block.id ? dropTarget.placement : null;
|
|
320
|
+
const itemClasses = [
|
|
321
|
+
'squisq-outline-item',
|
|
322
|
+
isDragging ? 'squisq-outline-item--dragging' : '',
|
|
323
|
+
dropPlacement ? `squisq-outline-item--drop-${dropPlacement}` : '',
|
|
324
|
+
]
|
|
325
|
+
.filter(Boolean)
|
|
326
|
+
.join(' ');
|
|
173
327
|
|
|
174
328
|
return (
|
|
175
|
-
<li className=
|
|
176
|
-
<div
|
|
329
|
+
<li className={itemClasses} role="treeitem" aria-current={isActive || undefined}>
|
|
330
|
+
<div
|
|
331
|
+
className="squisq-outline-row-wrap"
|
|
332
|
+
onDragOver={section ? (event) => onDragOver(event, section) : undefined}
|
|
333
|
+
onDrop={section ? (event) => onDrop(event, section) : undefined}
|
|
334
|
+
>
|
|
177
335
|
<button
|
|
178
336
|
type="button"
|
|
179
337
|
className={`squisq-outline-row squisq-outline-row--depth-${depth}${
|
|
180
338
|
isActive ? ' squisq-outline-row--current' : ''
|
|
181
339
|
}`}
|
|
182
340
|
onClick={() => onSelect(block)}
|
|
341
|
+
draggable={canDrag}
|
|
342
|
+
aria-grabbed={isDragging || undefined}
|
|
343
|
+
onDragStart={section ? (event) => onDragStart(event, section) : undefined}
|
|
344
|
+
onDragEnd={onDragEnd}
|
|
183
345
|
title={text || '(empty heading)'}
|
|
184
346
|
>
|
|
347
|
+
{canDrag && (
|
|
348
|
+
<span className="squisq-outline-drag-handle" aria-hidden="true">
|
|
349
|
+
<svg width="8" height="12" viewBox="0 0 8 12">
|
|
350
|
+
<circle cx="2" cy="2" r="1" fill="currentColor" />
|
|
351
|
+
<circle cx="6" cy="2" r="1" fill="currentColor" />
|
|
352
|
+
<circle cx="2" cy="6" r="1" fill="currentColor" />
|
|
353
|
+
<circle cx="6" cy="6" r="1" fill="currentColor" />
|
|
354
|
+
<circle cx="2" cy="10" r="1" fill="currentColor" />
|
|
355
|
+
<circle cx="6" cy="10" r="1" fill="currentColor" />
|
|
356
|
+
</svg>
|
|
357
|
+
</span>
|
|
358
|
+
)}
|
|
185
359
|
<span className="squisq-outline-row-text">{text || '(untitled)'}</span>
|
|
186
360
|
{showChip && (
|
|
187
361
|
<span className="squisq-outline-template-chip">{templateLabel(tplName!)}</span>
|
|
@@ -194,7 +368,7 @@ function OutlineNode({
|
|
|
194
368
|
className="squisq-outline-row-arrow"
|
|
195
369
|
aria-label={`Promote heading (currently H${depth})`}
|
|
196
370
|
title="Promote heading"
|
|
197
|
-
disabled={!canPromote}
|
|
371
|
+
disabled={mutationsDisabled || !canPromote}
|
|
198
372
|
onClick={() => onChangeLevel(block, -1)}
|
|
199
373
|
>
|
|
200
374
|
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
@@ -213,7 +387,7 @@ function OutlineNode({
|
|
|
213
387
|
className="squisq-outline-row-arrow"
|
|
214
388
|
aria-label={`Demote heading (currently H${depth})`}
|
|
215
389
|
title="Demote heading"
|
|
216
|
-
disabled={!canDemote}
|
|
390
|
+
disabled={mutationsDisabled || !canDemote}
|
|
217
391
|
onClick={() => onChangeLevel(block, +1)}
|
|
218
392
|
>
|
|
219
393
|
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
@@ -236,9 +410,17 @@ function OutlineNode({
|
|
|
236
410
|
<OutlineNode
|
|
237
411
|
key={child.id}
|
|
238
412
|
block={child}
|
|
413
|
+
parentId={block.id}
|
|
239
414
|
activeBlockId={activeBlockId}
|
|
415
|
+
draggedBlockId={draggedBlockId}
|
|
416
|
+
dropTarget={dropTarget}
|
|
417
|
+
mutationsDisabled={mutationsDisabled}
|
|
240
418
|
onSelect={onSelect}
|
|
241
419
|
onChangeLevel={onChangeLevel}
|
|
420
|
+
onDragStart={onDragStart}
|
|
421
|
+
onDragOver={onDragOver}
|
|
422
|
+
onDrop={onDrop}
|
|
423
|
+
onDragEnd={onDragEnd}
|
|
242
424
|
/>
|
|
243
425
|
))}
|
|
244
426
|
</ul>
|
|
@@ -323,6 +505,19 @@ function useActiveOutlineBlockId(): string | null {
|
|
|
323
505
|
|
|
324
506
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
325
507
|
|
|
508
|
+
function canDropOnSection(dragged: OutlineSectionRef, target: OutlineSectionRef): boolean {
|
|
509
|
+
return (
|
|
510
|
+
dragged.blockId !== target.blockId &&
|
|
511
|
+
dragged.depth === target.depth &&
|
|
512
|
+
dragged.parentId === target.parentId
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function placementForPointer(event: ReactDragEvent<HTMLElement>): OutlineDropPlacement {
|
|
517
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
518
|
+
return event.clientY < bounds.top + bounds.height / 2 ? 'before' : 'after';
|
|
519
|
+
}
|
|
520
|
+
|
|
326
521
|
function hasAnyHeading(blocks: Block[]): boolean {
|
|
327
522
|
for (const b of blocks) {
|
|
328
523
|
if (b.sourceHeading) return true;
|
package/src/PlainHtmlPreview.tsx
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* it can fetch.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
20
20
|
import type { CSSProperties } from 'react';
|
|
21
21
|
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
22
22
|
import type { MarkdownDocument, HtmlNode } from '@bendyline/squisq/markdown';
|
|
@@ -68,6 +68,8 @@ export interface PlainHtmlPreviewProps {
|
|
|
68
68
|
theme?: Theme;
|
|
69
69
|
className?: string;
|
|
70
70
|
style?: CSSProperties;
|
|
71
|
+
/** Let unmodified Up/Down arrows scroll the preview without requiring iframe focus. */
|
|
72
|
+
globalKeyboardShortcuts?: boolean;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const IFRAME_STYLE: CSSProperties = {
|
|
@@ -87,7 +89,9 @@ export function PlainHtmlPreview({
|
|
|
87
89
|
theme,
|
|
88
90
|
className,
|
|
89
91
|
style,
|
|
92
|
+
globalKeyboardShortcuts = false,
|
|
90
93
|
}: PlainHtmlPreviewProps) {
|
|
94
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
91
95
|
const mdDoc = useMemo<MarkdownDocument>(() => parseMarkdown(markdown), [markdown]);
|
|
92
96
|
|
|
93
97
|
// Resolve any relative image URLs the doc references. Blob URLs are
|
|
@@ -161,8 +165,43 @@ export function PlainHtmlPreview({
|
|
|
161
165
|
[renderFn, mdDoc, title, mergedImages, theme, iconsCss],
|
|
162
166
|
);
|
|
163
167
|
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!globalKeyboardShortcuts) return;
|
|
170
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
171
|
+
if (
|
|
172
|
+
event.defaultPrevented ||
|
|
173
|
+
event.altKey ||
|
|
174
|
+
event.ctrlKey ||
|
|
175
|
+
event.metaKey ||
|
|
176
|
+
event.shiftKey ||
|
|
177
|
+
(event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
|
178
|
+
) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
182
|
+
if (
|
|
183
|
+
target?.closest(
|
|
184
|
+
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], .monaco-editor',
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const frameWindow = iframeRef.current?.contentWindow;
|
|
190
|
+
if (!frameWindow) return;
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
const distance = Math.max(64, Math.round(iframeRef.current?.clientHeight ?? 0) * 0.12);
|
|
193
|
+
frameWindow.scrollBy({
|
|
194
|
+
top: event.key === 'ArrowDown' ? distance : -distance,
|
|
195
|
+
behavior: 'smooth',
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
199
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
200
|
+
}, [globalKeyboardShortcuts]);
|
|
201
|
+
|
|
164
202
|
return (
|
|
165
203
|
<iframe
|
|
204
|
+
ref={iframeRef}
|
|
166
205
|
className={className}
|
|
167
206
|
data-testid="plain-html-preview"
|
|
168
207
|
title={title ?? 'HTML preview'}
|