@bendyline/squisq-editor-react 1.6.2 → 2.0.1
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 +1275 -183
- package/dist/index.js +20345 -12777
- 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 +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +357 -51
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- 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 +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -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__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaBinDrop.test.tsx +90 -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 +382 -5
- 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__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -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 +254 -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 +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -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/codeContext/types.ts +1 -1
- 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/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- 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 +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- package/src/headingTransition.ts +0 -15
- 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 +198 -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/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/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- 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 +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -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 +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -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 +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -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 -272
- package/src/diagram/useDiagramData.ts +0 -126
package/src/PreviewPanel.tsx
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* in sync.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useEffect } from 'react';
|
|
11
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import { createPortal } from 'react-dom';
|
|
12
14
|
import { DocPlayer, LinearDocView, useMediaProvider } from '@bendyline/squisq-react';
|
|
15
|
+
import type { AudioController, PlaybackState } from '@bendyline/squisq-react';
|
|
13
16
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
14
17
|
import { applyTransform } from '@bendyline/squisq/transform';
|
|
15
18
|
import { resolveAudioMapping } from '@bendyline/squisq/doc';
|
|
@@ -17,7 +20,10 @@ import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
|
17
20
|
import { useEditorContext } from './EditorContext';
|
|
18
21
|
import { usePreviewSettings } from './PreviewControls';
|
|
19
22
|
import { buildPreviewDoc } from './buildPreviewDoc';
|
|
23
|
+
import { buildDocumentPreviewMarkdown } from './buildDocumentPreviewMarkdown';
|
|
20
24
|
import { PlainHtmlPreview } from './PlainHtmlPreview';
|
|
25
|
+
import { TeleprompterView } from './teleprompter/TeleprompterView';
|
|
26
|
+
import { usePresentationModeOptional } from './presentation/PresentationMode';
|
|
21
27
|
|
|
22
28
|
export interface PreviewPanelProps {
|
|
23
29
|
/** Base path for resolving media URLs in DocPlayer */
|
|
@@ -40,8 +46,19 @@ export interface PreviewPanelProps {
|
|
|
40
46
|
* are rendered in the main toolbar via PreviewToolbarControls.
|
|
41
47
|
*/
|
|
42
48
|
export function PreviewPanel({ basePath = '/', className, workspaceContainer }: PreviewPanelProps) {
|
|
43
|
-
const {
|
|
49
|
+
const {
|
|
50
|
+
doc,
|
|
51
|
+
parseError,
|
|
52
|
+
isParsing,
|
|
53
|
+
markdownSource,
|
|
54
|
+
mediaRevision,
|
|
55
|
+
setMarkdownSource,
|
|
56
|
+
bumpMediaRevision,
|
|
57
|
+
allowRecording,
|
|
58
|
+
colorScheme,
|
|
59
|
+
} = useEditorContext();
|
|
44
60
|
const mediaProvider = useMediaProvider();
|
|
61
|
+
const presentation = usePresentationModeOptional();
|
|
45
62
|
const {
|
|
46
63
|
activeViewport,
|
|
47
64
|
activeDisplayMode,
|
|
@@ -51,6 +68,13 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
51
68
|
activeCaptionsEnabled,
|
|
52
69
|
activeCoverSlide,
|
|
53
70
|
} = usePreviewSettings();
|
|
71
|
+
const mainSurfaceRef = useRef<HTMLDivElement>(null);
|
|
72
|
+
const popupSurfaceRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
const [playbackState, setPlaybackState] = useState<PlaybackState | null>(null);
|
|
74
|
+
|
|
75
|
+
const handlePlaybackStateChange = useCallback((next: PlaybackState) => {
|
|
76
|
+
setPlaybackState(next);
|
|
77
|
+
}, []);
|
|
54
78
|
|
|
55
79
|
// Build the player-ready Doc whenever the parsed doc changes.
|
|
56
80
|
// Transform runs on the ORIGINAL doc (which has block.contents with
|
|
@@ -60,40 +84,174 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
60
84
|
// Audio mapping is async (reads container files), so we use a two-phase
|
|
61
85
|
// approach: first build the base doc synchronously, then resolve audio
|
|
62
86
|
// in an effect and update the state.
|
|
63
|
-
const [
|
|
87
|
+
const [previewProjection, setPreviewProjection] = useState<{
|
|
88
|
+
/** Transformed content model shared by Page and Document. */
|
|
89
|
+
contentDoc: Doc;
|
|
90
|
+
/** Flattened/timed player model shared by Slideshow and Video. */
|
|
91
|
+
playerDoc: Doc;
|
|
92
|
+
} | null>(null);
|
|
64
93
|
|
|
65
94
|
useEffect(() => {
|
|
66
95
|
if (!doc || !doc.blocks.length) {
|
|
67
|
-
|
|
96
|
+
setPreviewProjection(null);
|
|
68
97
|
return;
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
// Audio resolution runs BEFORE the transform: a document-anchored
|
|
101
|
+
// narration take re-times the SOURCE blocks, and the transform's
|
|
102
|
+
// provenance (`sourceStartTime` in seconds) is derived from those
|
|
103
|
+
// times — so summarized slides land where the words are spoken.
|
|
104
|
+
const build = (sourceDoc: Doc) => {
|
|
105
|
+
const contentDoc = activeTransformStyle
|
|
106
|
+
? applyTransform(sourceDoc, activeTransformStyle).doc
|
|
107
|
+
: sourceDoc;
|
|
108
|
+
return { contentDoc, playerDoc: buildPreviewDoc(contentDoc) };
|
|
109
|
+
};
|
|
76
110
|
|
|
77
|
-
// If we have a workspace container, try to resolve audio mapping before building preview
|
|
78
111
|
if (workspaceContainer) {
|
|
79
112
|
let cancelled = false;
|
|
80
|
-
resolveAudioMapping(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
113
|
+
resolveAudioMapping(doc, workspaceContainer).then(
|
|
114
|
+
(audioDoc) => {
|
|
115
|
+
if (!cancelled) setPreviewProjection(build(audioDoc));
|
|
116
|
+
},
|
|
117
|
+
() => {
|
|
118
|
+
// The synchronous preview below remains valid when optional audio
|
|
119
|
+
// discovery fails; consume the rejection rather than leaking it.
|
|
120
|
+
},
|
|
121
|
+
);
|
|
85
122
|
// Set an immediate preview without audio while mapping resolves
|
|
86
|
-
|
|
123
|
+
setPreviewProjection(build(doc));
|
|
87
124
|
return () => {
|
|
88
125
|
cancelled = true;
|
|
89
126
|
};
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
|
|
129
|
+
setPreviewProjection(build(doc));
|
|
93
130
|
}, [doc, activeTransformStyle, workspaceContainer]);
|
|
94
131
|
|
|
95
|
-
|
|
96
|
-
|
|
132
|
+
const previewDoc = previewProjection?.playerDoc ?? null;
|
|
133
|
+
const contentDoc = previewProjection?.contentDoc ?? null;
|
|
134
|
+
|
|
135
|
+
// The popup owns no clock, audio, or transport. It follows the primary
|
|
136
|
+
// player's published state while all actions remain inert, keeping the
|
|
137
|
+
// opener the single source of truth.
|
|
138
|
+
const followerAudioController = useMemo<AudioController | null>(() => {
|
|
139
|
+
if (!playbackState || !previewDoc) return null;
|
|
140
|
+
const noOp = () => Promise.resolve();
|
|
141
|
+
return {
|
|
142
|
+
currentTime: playbackState.currentTime,
|
|
143
|
+
isPlaying: playbackState.isPlaying,
|
|
144
|
+
currentSegment: playbackState.currentSegmentIndex,
|
|
145
|
+
totalDuration: playbackState.totalDuration,
|
|
146
|
+
isEnded:
|
|
147
|
+
playbackState.totalDuration > 0 && playbackState.currentTime >= playbackState.totalDuration,
|
|
148
|
+
isReady: true,
|
|
149
|
+
isAvailable: true,
|
|
150
|
+
play: noOp,
|
|
151
|
+
pause: noOp,
|
|
152
|
+
toggle: noOp,
|
|
153
|
+
seekTo: noOp,
|
|
154
|
+
skipToSegment: noOp,
|
|
155
|
+
restart: noOp,
|
|
156
|
+
};
|
|
157
|
+
}, [playbackState, previewDoc]);
|
|
158
|
+
|
|
159
|
+
const documentMarkdown = useMemo(
|
|
160
|
+
() =>
|
|
161
|
+
activeTransformStyle && contentDoc
|
|
162
|
+
? buildDocumentPreviewMarkdown(contentDoc)
|
|
163
|
+
: markdownSource,
|
|
164
|
+
[activeTransformStyle, contentDoc, markdownSource],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// The public DisplayMode values predate the current labels: raw `page`
|
|
168
|
+
// is the plain Document preview, while raw `linear` is the styled Page view.
|
|
169
|
+
const isDocumentMode = activeDisplayMode === 'page';
|
|
170
|
+
const isPageMode = activeDisplayMode === 'linear';
|
|
171
|
+
const isNarrateMode = activeDisplayMode === 'narrate';
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (presentation?.activeTarget === 'window') return;
|
|
175
|
+
setPlaybackState(null);
|
|
176
|
+
}, [presentation?.activeTarget]);
|
|
177
|
+
|
|
178
|
+
// Page and Document have no playback clock, so mirror their normalized
|
|
179
|
+
// scroll position instead. The popup remains pointer-inert: the editor view
|
|
180
|
+
// is the controller and the audience window follows it.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (presentation?.activeTarget !== 'window' || !presentation.popupRoot) return;
|
|
183
|
+
const mainRoot = mainSurfaceRef.current;
|
|
184
|
+
const followerRoot = popupSurfaceRef.current;
|
|
185
|
+
if (!mainRoot || !followerRoot) return;
|
|
186
|
+
|
|
187
|
+
if (isPageMode) {
|
|
188
|
+
const source = mainRoot.querySelector<HTMLElement>('.squisq-linear');
|
|
189
|
+
const follower = followerRoot.querySelector<HTMLElement>('.squisq-linear');
|
|
190
|
+
if (!source || !follower) return;
|
|
191
|
+
const sync = () => {
|
|
192
|
+
const sourceRange = Math.max(1, source.scrollHeight - source.clientHeight);
|
|
193
|
+
const followerRange = Math.max(0, follower.scrollHeight - follower.clientHeight);
|
|
194
|
+
follower.scrollTop = (source.scrollTop / sourceRange) * followerRange;
|
|
195
|
+
};
|
|
196
|
+
source.addEventListener('scroll', sync, { passive: true });
|
|
197
|
+
sync();
|
|
198
|
+
return () => source.removeEventListener('scroll', sync);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isDocumentMode) {
|
|
202
|
+
const sourceFrame = mainRoot.querySelector<HTMLIFrameElement>('iframe');
|
|
203
|
+
const followerFrame = followerRoot.querySelector<HTMLIFrameElement>('iframe');
|
|
204
|
+
if (!sourceFrame || !followerFrame) return;
|
|
205
|
+
let detachScroll: (() => void) | null = null;
|
|
206
|
+
const attach = () => {
|
|
207
|
+
detachScroll?.();
|
|
208
|
+
try {
|
|
209
|
+
const sourceWindow = sourceFrame.contentWindow;
|
|
210
|
+
const followerWindow = followerFrame.contentWindow;
|
|
211
|
+
if (!sourceWindow || !followerWindow) return;
|
|
212
|
+
const sync = () => {
|
|
213
|
+
const sourceDoc = sourceWindow.document.documentElement;
|
|
214
|
+
const followerDoc = followerWindow.document.documentElement;
|
|
215
|
+
const sourceRange = Math.max(1, sourceDoc.scrollHeight - sourceWindow.innerHeight);
|
|
216
|
+
const followerRange = Math.max(
|
|
217
|
+
0,
|
|
218
|
+
followerDoc.scrollHeight - followerWindow.innerHeight,
|
|
219
|
+
);
|
|
220
|
+
followerWindow.scrollTo(0, (sourceWindow.scrollY / sourceRange) * followerRange);
|
|
221
|
+
};
|
|
222
|
+
sourceWindow.addEventListener('scroll', sync, { passive: true });
|
|
223
|
+
detachScroll = () => sourceWindow.removeEventListener('scroll', sync);
|
|
224
|
+
sync();
|
|
225
|
+
} catch {
|
|
226
|
+
// The srcDoc frames are same-origin, but a host may tighten their
|
|
227
|
+
// sandbox. In that case the duplicate content remains useful even
|
|
228
|
+
// though scroll mirroring is unavailable.
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
sourceFrame.addEventListener('load', attach);
|
|
232
|
+
followerFrame.addEventListener('load', attach);
|
|
233
|
+
attach();
|
|
234
|
+
return () => {
|
|
235
|
+
sourceFrame.removeEventListener('load', attach);
|
|
236
|
+
followerFrame.removeEventListener('load', attach);
|
|
237
|
+
detachScroll?.();
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}, [
|
|
241
|
+
contentDoc,
|
|
242
|
+
documentMarkdown,
|
|
243
|
+
isDocumentMode,
|
|
244
|
+
isPageMode,
|
|
245
|
+
presentation?.activeTarget,
|
|
246
|
+
presentation?.popupRoot,
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
// Status overlays for non-ready states. Narrate is exempt: the
|
|
250
|
+
// teleprompter holds live state (mic capture, an in-flight recording,
|
|
251
|
+
// pacing position) that an unmount would destroy — e.g. saving a take
|
|
252
|
+
// rewrites the markdown, which triggers exactly this reparse. It keeps
|
|
253
|
+
// rendering the last-good doc until the new parse lands.
|
|
254
|
+
if (isParsing && !isNarrateMode) {
|
|
97
255
|
return (
|
|
98
256
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
99
257
|
<p>Parsing…</p>
|
|
@@ -101,7 +259,7 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
101
259
|
);
|
|
102
260
|
}
|
|
103
261
|
|
|
104
|
-
if (parseError) {
|
|
262
|
+
if (parseError && !isNarrateMode) {
|
|
105
263
|
return (
|
|
106
264
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
107
265
|
<h3>Parse Error</h3>
|
|
@@ -110,15 +268,11 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
110
268
|
);
|
|
111
269
|
}
|
|
112
270
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Document mode renders directly from markdown — it doesn't depend on the
|
|
119
|
-
// parsed Doc tree or the player preview build, so let it fall through even
|
|
120
|
-
// when those aren't ready yet.
|
|
121
|
-
if (!previewDoc && !isDocumentMode) {
|
|
271
|
+
// Document mode renders a text-first markdown projection, and Narrate
|
|
272
|
+
// builds its script from the parsed source doc (showing its own empty
|
|
273
|
+
// state). Neither depends on the player projection, so let them fall
|
|
274
|
+
// through even when that isn't ready yet.
|
|
275
|
+
if (!previewDoc && !isDocumentMode && !isNarrateMode) {
|
|
122
276
|
return (
|
|
123
277
|
<div className={`squisq-preview-status ${className || ''}`} data-testid="preview-panel">
|
|
124
278
|
<p>No content to preview. Start typing in the editor.</p>
|
|
@@ -126,63 +280,143 @@ export function PreviewPanel({ basePath = '/', className, workspaceContainer }:
|
|
|
126
280
|
);
|
|
127
281
|
}
|
|
128
282
|
|
|
129
|
-
const fillsContainer = isDocumentMode || isPageMode ? 'stretch' : 'center';
|
|
283
|
+
const fillsContainer = isDocumentMode || isPageMode || isNarrateMode ? 'stretch' : 'center';
|
|
284
|
+
const audienceWindowOpen = presentation?.activeTarget === 'window';
|
|
285
|
+
|
|
286
|
+
const renderSurface = (audience: boolean): ReactNode => {
|
|
287
|
+
if (isDocumentMode) {
|
|
288
|
+
return (
|
|
289
|
+
<PlainHtmlPreview
|
|
290
|
+
markdown={documentMarkdown}
|
|
291
|
+
title={(contentDoc?.frontmatter?.title as string | undefined) ?? undefined}
|
|
292
|
+
mediaProvider={mediaProvider}
|
|
293
|
+
mediaRevision={mediaRevision}
|
|
294
|
+
theme={activeTheme}
|
|
295
|
+
globalKeyboardShortcuts={!audience}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isNarrateMode) {
|
|
301
|
+
if (audience) return null;
|
|
302
|
+
// Narrate keeps one controller/microphone session in the main view and
|
|
303
|
+
// portals only its display surface to the audience window.
|
|
304
|
+
return (
|
|
305
|
+
<TeleprompterView
|
|
306
|
+
doc={doc}
|
|
307
|
+
theme={activeTheme}
|
|
308
|
+
workspaceContainer={workspaceContainer ?? null}
|
|
309
|
+
basePath={basePath}
|
|
310
|
+
presentationTarget={
|
|
311
|
+
presentation?.activeTarget === 'window' ? presentation.popupRoot : null
|
|
312
|
+
}
|
|
313
|
+
recording={
|
|
314
|
+
allowRecording && mediaProvider
|
|
315
|
+
? {
|
|
316
|
+
mediaProvider,
|
|
317
|
+
container: workspaceContainer ?? null,
|
|
318
|
+
markdownSource,
|
|
319
|
+
setMarkdownSource,
|
|
320
|
+
bumpMediaRevision,
|
|
321
|
+
}
|
|
322
|
+
: null
|
|
323
|
+
}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (isPageMode) {
|
|
329
|
+
return (
|
|
330
|
+
<LinearDocView
|
|
331
|
+
doc={contentDoc ?? doc!}
|
|
332
|
+
basePath={basePath}
|
|
333
|
+
viewport={activeViewport}
|
|
334
|
+
theme={activeTheme}
|
|
335
|
+
globalKeyboardShortcuts={!audience}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (audience && !followerAudioController) {
|
|
341
|
+
return <div className="squisq-presentation-connecting">Connecting presentation...</div>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const audienceCaptionMode = playbackState?.captionMode;
|
|
345
|
+
return (
|
|
346
|
+
<DocPlayer
|
|
347
|
+
// A style change replaces the slide sequence. Remounting clears state
|
|
348
|
+
// belonging to the previous transformed deck in both render targets.
|
|
349
|
+
key={`${audience ? 'audience' : 'primary'}-${activeTransformStyle || 'none'}`}
|
|
350
|
+
doc={previewDoc!}
|
|
351
|
+
basePath={basePath}
|
|
352
|
+
showControls={!audience}
|
|
353
|
+
muted
|
|
354
|
+
forceViewport={activeViewport}
|
|
355
|
+
displayMode={activeDisplayMode}
|
|
356
|
+
theme={activeTheme}
|
|
357
|
+
captionStyle={audienceCaptionMode === 'social' ? 'social' : activeCaptionStyle}
|
|
358
|
+
captionsEnabled={
|
|
359
|
+
audience && audienceCaptionMode ? audienceCaptionMode !== 'off' : activeCaptionsEnabled
|
|
360
|
+
}
|
|
361
|
+
showCoverSlide={activeCoverSlide}
|
|
362
|
+
coverVisible={audience ? playbackState?.isCoverVisible : undefined}
|
|
363
|
+
audioController={audience ? followerAudioController! : undefined}
|
|
364
|
+
enableSwipe={!audience}
|
|
365
|
+
globalKeyboardShortcuts={!audience}
|
|
366
|
+
onPlaybackStateChange={
|
|
367
|
+
!audience && audienceWindowOpen ? handlePlaybackStateChange : undefined
|
|
368
|
+
}
|
|
369
|
+
/>
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const surfaceStyle = {
|
|
374
|
+
flex: 1,
|
|
375
|
+
display: 'flex',
|
|
376
|
+
alignItems: fillsContainer,
|
|
377
|
+
justifyContent: 'center',
|
|
378
|
+
overflow: 'hidden',
|
|
379
|
+
minHeight: 0,
|
|
380
|
+
} as const;
|
|
130
381
|
|
|
131
382
|
return (
|
|
132
|
-
|
|
133
|
-
className={`squisq-preview-container ${className || ''}`}
|
|
134
|
-
data-testid="preview-panel"
|
|
135
|
-
style={{
|
|
136
|
-
width: '100%',
|
|
137
|
-
height: '100%',
|
|
138
|
-
display: 'flex',
|
|
139
|
-
flexDirection: 'column',
|
|
140
|
-
overflow: 'hidden',
|
|
141
|
-
background: 'var(--squisq-bg, #f5f5f5)',
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
144
|
-
{/* Player / Document / Page view */}
|
|
383
|
+
<>
|
|
145
384
|
<div
|
|
146
|
-
className=
|
|
385
|
+
className={`squisq-preview-container ${className || ''}`}
|
|
386
|
+
data-testid="preview-panel"
|
|
147
387
|
style={{
|
|
148
|
-
|
|
388
|
+
width: '100%',
|
|
389
|
+
height: '100%',
|
|
149
390
|
display: 'flex',
|
|
150
|
-
|
|
151
|
-
justifyContent: 'center',
|
|
391
|
+
flexDirection: 'column',
|
|
152
392
|
overflow: 'hidden',
|
|
153
|
-
|
|
393
|
+
background: 'var(--squisq-bg, #f5f5f5)',
|
|
154
394
|
}}
|
|
155
395
|
>
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
title={(doc?.frontmatter?.title as string | undefined) ?? undefined}
|
|
160
|
-
mediaProvider={mediaProvider}
|
|
161
|
-
mediaRevision={mediaRevision}
|
|
162
|
-
theme={activeTheme}
|
|
163
|
-
/>
|
|
164
|
-
) : isPageMode ? (
|
|
165
|
-
<LinearDocView
|
|
166
|
-
doc={doc!}
|
|
167
|
-
basePath={basePath}
|
|
168
|
-
viewport={activeViewport}
|
|
169
|
-
theme={activeTheme}
|
|
170
|
-
/>
|
|
171
|
-
) : (
|
|
172
|
-
<DocPlayer
|
|
173
|
-
doc={previewDoc!}
|
|
174
|
-
basePath={basePath}
|
|
175
|
-
showControls
|
|
176
|
-
muted
|
|
177
|
-
forceViewport={activeViewport}
|
|
178
|
-
displayMode={activeDisplayMode}
|
|
179
|
-
theme={activeTheme}
|
|
180
|
-
captionStyle={activeCaptionStyle}
|
|
181
|
-
captionsEnabled={activeCaptionsEnabled}
|
|
182
|
-
showCoverSlide={activeCoverSlide}
|
|
183
|
-
/>
|
|
184
|
-
)}
|
|
396
|
+
<div ref={mainSurfaceRef} className="squisq-preview-player" style={surfaceStyle}>
|
|
397
|
+
{renderSurface(false)}
|
|
398
|
+
</div>
|
|
185
399
|
</div>
|
|
186
|
-
|
|
400
|
+
{audienceWindowOpen && presentation?.popupRoot && !isNarrateMode
|
|
401
|
+
? createPortal(
|
|
402
|
+
<div
|
|
403
|
+
className="squisq-editor-shell squisq-presentation-window"
|
|
404
|
+
data-theme={colorScheme}
|
|
405
|
+
>
|
|
406
|
+
<div className="squisq-preview-container squisq-presentation-window-preview">
|
|
407
|
+
<div
|
|
408
|
+
ref={popupSurfaceRef}
|
|
409
|
+
className="squisq-preview-player squisq-presentation-window-surface"
|
|
410
|
+
style={surfaceStyle}
|
|
411
|
+
aria-label="Audience presentation"
|
|
412
|
+
>
|
|
413
|
+
{renderSurface(true)}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>,
|
|
417
|
+
presentation.popupRoot,
|
|
418
|
+
)
|
|
419
|
+
: null}
|
|
420
|
+
</>
|
|
187
421
|
);
|
|
188
422
|
}
|
package/src/RawEditor.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { getAvailableTemplates } from '@bendyline/squisq/doc';
|
|
|
14
14
|
import { suggestIcons, resolveIcon, iconGlyph } from '@bendyline/squisq/icons';
|
|
15
15
|
import { BLOCK_META_KEY_DESCRIPTORS, tokenizeAttrTokens } from '@bendyline/squisq/markdown';
|
|
16
16
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
17
|
+
import { canHandleSquisqMediaDrop, ownsMonacoModel } from './rawEditorIsolation';
|
|
17
18
|
import { useMonacoLoader } from './useMonacoLoader';
|
|
18
19
|
|
|
19
20
|
// Monaco is loaded lazily through `useMonacoLoader` (see the hook for the
|
|
@@ -108,9 +109,13 @@ export function RawEditor({
|
|
|
108
109
|
const keyDisposable = useRef<monaco.IDisposable | null>(null);
|
|
109
110
|
// Ref so the keydown handler always sees the latest callback.
|
|
110
111
|
const submitOnEnterRef = useRef(submitOnEnter);
|
|
112
|
+
const readOnlyRef = useRef(readOnly);
|
|
111
113
|
useEffect(() => {
|
|
112
114
|
submitOnEnterRef.current = submitOnEnter;
|
|
113
115
|
}, [submitOnEnter]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
readOnlyRef.current = readOnly;
|
|
118
|
+
}, [readOnly]);
|
|
114
119
|
// Ref so the completion provider — registered once at mount — always
|
|
115
120
|
// sees the latest mentionProvider without needing to unregister.
|
|
116
121
|
const mentionProviderRef = useRef(mentionProvider);
|
|
@@ -194,6 +199,7 @@ export function RawEditor({
|
|
|
194
199
|
completionDisposable.current = monaco.languages.registerCompletionItemProvider('markdown', {
|
|
195
200
|
triggerCharacters: ['['],
|
|
196
201
|
provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
|
202
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
197
203
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
198
204
|
|
|
199
205
|
// Only trigger inside a heading line that has {[ before the cursor
|
|
@@ -248,6 +254,7 @@ export function RawEditor({
|
|
|
248
254
|
{
|
|
249
255
|
triggerCharacters: ['@'],
|
|
250
256
|
async provideCompletionItems(model, position) {
|
|
257
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
251
258
|
const provider = mentionProviderRef.current;
|
|
252
259
|
if (!provider) return { suggestions: [] };
|
|
253
260
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
@@ -306,6 +313,7 @@ export function RawEditor({
|
|
|
306
313
|
{
|
|
307
314
|
triggerCharacters: ['['],
|
|
308
315
|
provideCompletionItems(model, position) {
|
|
316
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
309
317
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
310
318
|
const textBeforeCursor = lineContent.substring(0, position.column - 1);
|
|
311
319
|
const textAfterCursor = lineContent.substring(position.column - 1);
|
|
@@ -388,6 +396,7 @@ export function RawEditor({
|
|
|
388
396
|
{
|
|
389
397
|
triggerCharacters: ['=', ' '],
|
|
390
398
|
provideCompletionItems(model, position) {
|
|
399
|
+
if (!ownsMonacoModel(editor, model)) return { suggestions: [] };
|
|
391
400
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
392
401
|
// Block-meta attributes only mean something on a heading's
|
|
393
402
|
// template annotation — same gate as the template provider.
|
|
@@ -498,12 +507,14 @@ export function RawEditor({
|
|
|
498
507
|
const domNode = editor.getDomNode();
|
|
499
508
|
if (domNode) {
|
|
500
509
|
const onDragOver = (e: DragEvent) => {
|
|
501
|
-
|
|
510
|
+
const dt = e.dataTransfer;
|
|
511
|
+
if (canHandleSquisqMediaDrop(readOnlyRef.current, dt)) {
|
|
502
512
|
e.preventDefault();
|
|
503
|
-
|
|
513
|
+
dt!.dropEffect = 'copy';
|
|
504
514
|
}
|
|
505
515
|
};
|
|
506
516
|
const onDrop = (e: DragEvent) => {
|
|
517
|
+
if (!canHandleSquisqMediaDrop(readOnlyRef.current, e.dataTransfer)) return;
|
|
507
518
|
const dt = e.dataTransfer;
|
|
508
519
|
if (!dt) return;
|
|
509
520
|
const raw = dt.getData(SQUISQ_MEDIA_MIME);
|
package/src/RecorderEntry.tsx
CHANGED
|
@@ -81,6 +81,7 @@ export function RecorderEntry() {
|
|
|
81
81
|
bumpMediaRevision,
|
|
82
82
|
markdownSource,
|
|
83
83
|
setMarkdownSource,
|
|
84
|
+
colorScheme,
|
|
84
85
|
} = useEditorContext();
|
|
85
86
|
|
|
86
87
|
const handleSave = useCallback(
|
|
@@ -150,6 +151,7 @@ export function RecorderEntry() {
|
|
|
150
151
|
<RecorderPanel
|
|
151
152
|
mediaProvider={mediaProvider}
|
|
152
153
|
container={workspaceContainer}
|
|
154
|
+
colorScheme={colorScheme}
|
|
153
155
|
onSave={handleSave}
|
|
154
156
|
className="squisq-toolbar-button"
|
|
155
157
|
/>
|
package/src/StatusBar.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useMemo } from 'react';
|
|
8
|
+
import { countBlocks } from '@bendyline/squisq/doc';
|
|
8
9
|
import { useEditorContext } from './EditorContext';
|
|
9
10
|
|
|
10
11
|
export interface StatusBarProps {
|
|
@@ -23,7 +24,7 @@ export function StatusBar({ className }: StatusBarProps) {
|
|
|
23
24
|
const chars = markdownSource.length;
|
|
24
25
|
const words = markdownSource.trim() ? markdownSource.trim().split(/\s+/).length : 0;
|
|
25
26
|
const lines = markdownSource.split('\n').length;
|
|
26
|
-
const blocks = doc
|
|
27
|
+
const blocks = doc ? countBlocks(doc.blocks) : 0;
|
|
27
28
|
return { chars, words, lines, blocks };
|
|
28
29
|
}, [markdownSource, doc]);
|
|
29
30
|
|
|
@@ -32,7 +33,9 @@ export function StatusBar({ className }: StatusBarProps) {
|
|
|
32
33
|
<span className="squisq-status-item">{stats.words} words</span>
|
|
33
34
|
<span className="squisq-status-item">{stats.chars} chars</span>
|
|
34
35
|
<span className="squisq-status-item">{stats.lines} lines</span>
|
|
35
|
-
<span className="squisq-status-item">
|
|
36
|
+
<span className="squisq-status-item">
|
|
37
|
+
{stats.blocks} {stats.blocks === 1 ? 'block' : 'blocks'}
|
|
38
|
+
</span>
|
|
36
39
|
<span className="squisq-status-spacer" />
|
|
37
40
|
{isParsing && <span className="squisq-status-item squisq-status-parsing">Parsing…</span>}
|
|
38
41
|
{parseError && (
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TemplateAnnotation — Tiptap Heading Extension
|
|
3
3
|
*
|
|
4
|
-
* Extends Tiptap's built-in Heading node to support `data-template
|
|
5
|
-
* `data-template-params` HTML attributes. These
|
|
6
|
-
*
|
|
4
|
+
* Extends Tiptap's built-in Heading node to support `data-template`,
|
|
5
|
+
* `data-template-params`, and `data-template-empty` HTML attributes. These
|
|
6
|
+
* attributes store the authored template annotation without exposing its raw
|
|
7
|
+
* `{[…]}` syntax in the Write view.
|
|
7
8
|
*
|
|
8
9
|
* When present, the heading renders a visible badge (styled CSS chip)
|
|
9
10
|
* showing the template name, e.g. `[chart]`.
|
|
@@ -41,6 +42,14 @@ export const HeadingWithTemplate = Heading.extend({
|
|
|
41
42
|
return { 'data-template-params': attributes.dataTemplateParams };
|
|
42
43
|
},
|
|
43
44
|
},
|
|
45
|
+
dataTemplateEmpty: {
|
|
46
|
+
default: false,
|
|
47
|
+
parseHTML: (element: HTMLElement) => element.hasAttribute('data-template-empty'),
|
|
48
|
+
renderHTML: (attributes: Record<string, unknown>) => {
|
|
49
|
+
if (!attributes.dataTemplateEmpty) return {};
|
|
50
|
+
return { 'data-template-empty': 'true' };
|
|
51
|
+
},
|
|
52
|
+
},
|
|
44
53
|
dataBlockAttrs: {
|
|
45
54
|
default: null,
|
|
46
55
|
parseHTML: (element: HTMLElement) => element.getAttribute('data-block-attrs') || null,
|