@nuasite/cms 0.19.0 → 0.20.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/dist/editor.js +11055 -10934
- package/package.json +3 -3
- package/src/build-processor.ts +4 -4
- package/src/dev-middleware.ts +171 -185
- package/src/editor/components/fields.tsx +6 -6
- package/src/editor/components/markdown-editor-overlay.tsx +41 -46
- package/src/editor/components/markdown-inline-editor.tsx +34 -165
- package/src/editor/components/mdx-block-view.tsx +351 -47
- package/src/editor/components/mdx-component-picker.tsx +35 -11
- package/src/editor/components/media-library.tsx +1 -15
- package/src/editor/components/modal-shell.tsx +1 -1
- package/src/editor/milkdown-mdx-plugin.tsx +116 -19
- package/src/editor/milkdown-utils.ts +174 -0
- package/src/editor/signals.ts +1 -18
- package/src/editor/types.ts +0 -10
- package/src/html-processor.ts +9 -7
- package/src/index.ts +3 -13
- package/src/source-finder/cache.ts +47 -0
- package/src/source-finder/collection-finder.ts +181 -0
- package/src/source-finder/index.ts +5 -2
- package/src/source-finder/search-index.ts +79 -0
- package/src/source-finder/snippet-utils.ts +36 -61
- package/src/utils.ts +10 -0
- package/src/vite-plugin.ts +24 -4
- package/src/editor/components/mdx-props-editor.tsx +0 -94
|
@@ -50,6 +50,7 @@ export function MarkdownEditorOverlay() {
|
|
|
50
50
|
// Preview mode state
|
|
51
51
|
const [isPreview, setIsPreview] = useState(false)
|
|
52
52
|
const originalHTMLRef = useRef<string | null>(null)
|
|
53
|
+
const previewTargetRef = useRef<HTMLElement | null>(null)
|
|
53
54
|
const editorInstanceRef = useRef<Editor | null>(null)
|
|
54
55
|
|
|
55
56
|
useEffect(() => {
|
|
@@ -58,6 +59,19 @@ export function MarkdownEditorOverlay() {
|
|
|
58
59
|
}
|
|
59
60
|
}, [isCreateMode, isDataCollection])
|
|
60
61
|
|
|
62
|
+
// Auto-generate slug from title in create mode (unless user manually edited the slug)
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!isCreateMode || slugManuallyEdited) return
|
|
65
|
+
const title = (page?.frontmatter.title as string) || (page?.frontmatter.name as string) || ''
|
|
66
|
+
if (!title) return
|
|
67
|
+
markdownEditorState.value = {
|
|
68
|
+
...markdownEditorState.value,
|
|
69
|
+
currentPage: markdownEditorState.value.currentPage
|
|
70
|
+
? { ...markdownEditorState.value.currentPage, slug: slugify(title), isDirty: true }
|
|
71
|
+
: null,
|
|
72
|
+
}
|
|
73
|
+
}, [isCreateMode, slugManuallyEdited, page?.frontmatter.title, page?.frontmatter.name])
|
|
74
|
+
|
|
61
75
|
const handleDeploymentComplete = useCallback(
|
|
62
76
|
(status: 'completed' | 'failed' | 'timeout') => {
|
|
63
77
|
if (status === 'failed') {
|
|
@@ -67,14 +81,21 @@ export function MarkdownEditorOverlay() {
|
|
|
67
81
|
[],
|
|
68
82
|
)
|
|
69
83
|
|
|
84
|
+
/** Find the [data-cms-markdown] wrapper element on the actual page (not CMS UI). */
|
|
85
|
+
const findMarkdownWrapper = useCallback((): HTMLElement | null => {
|
|
86
|
+
const SKIP_TAGS = new Set(['BODY', 'HTML', 'BUTTON', 'SPAN', 'A'])
|
|
87
|
+
const candidates = document.querySelectorAll('[data-cms-markdown]:not([data-cms-ui])')
|
|
88
|
+
for (const c of candidates) {
|
|
89
|
+
if (!SKIP_TAGS.has(c.tagName)) return c as HTMLElement
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
70
94
|
const restoreOriginalHTML = useCallback(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const el = document.querySelector(`[data-cms-id="${activeId}"]`)
|
|
74
|
-
if (el) {
|
|
75
|
-
el.innerHTML = originalHTMLRef.current
|
|
76
|
-
}
|
|
95
|
+
if (originalHTMLRef.current !== null && previewTargetRef.current) {
|
|
96
|
+
previewTargetRef.current.innerHTML = originalHTMLRef.current
|
|
77
97
|
originalHTMLRef.current = null
|
|
98
|
+
previewTargetRef.current = null
|
|
78
99
|
}
|
|
79
100
|
}, [])
|
|
80
101
|
|
|
@@ -95,11 +116,8 @@ export function MarkdownEditorOverlay() {
|
|
|
95
116
|
if (result.success) {
|
|
96
117
|
// Keep the preview HTML in place so user sees changes immediately
|
|
97
118
|
// If not in preview mode, inject editor HTML into the page element
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const el = document.querySelector(
|
|
101
|
-
`[data-cms-id="${activeId}"]`,
|
|
102
|
-
)
|
|
119
|
+
if (editorInstanceRef.current && !isPreview) {
|
|
120
|
+
const el = findMarkdownWrapper()
|
|
103
121
|
if (el) {
|
|
104
122
|
try {
|
|
105
123
|
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
@@ -133,7 +151,7 @@ export function MarkdownEditorOverlay() {
|
|
|
133
151
|
setIsSaving(false)
|
|
134
152
|
}
|
|
135
153
|
},
|
|
136
|
-
[isSaving, isPreview, handleDeploymentComplete],
|
|
154
|
+
[isSaving, isPreview, handleDeploymentComplete, findMarkdownWrapper],
|
|
137
155
|
)
|
|
138
156
|
|
|
139
157
|
const handleCreate = useCallback(async () => {
|
|
@@ -215,19 +233,21 @@ export function MarkdownEditorOverlay() {
|
|
|
215
233
|
if (!editorInstanceRef.current || !activeId) return
|
|
216
234
|
|
|
217
235
|
if (!isPreview) {
|
|
218
|
-
// Enter preview
|
|
219
|
-
const el =
|
|
236
|
+
// Enter preview — inject editor HTML into the markdown wrapper element.
|
|
237
|
+
const el = findMarkdownWrapper()
|
|
220
238
|
if (!el) {
|
|
221
239
|
showToast('Could not find page element to preview', 'error')
|
|
222
240
|
return
|
|
223
241
|
}
|
|
224
242
|
originalHTMLRef.current = el.innerHTML
|
|
243
|
+
previewTargetRef.current = el
|
|
225
244
|
try {
|
|
226
245
|
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
227
246
|
el.innerHTML = view.dom.innerHTML
|
|
228
247
|
} catch (error) {
|
|
229
248
|
console.error('Failed to get editor HTML for preview:', error)
|
|
230
249
|
originalHTMLRef.current = null
|
|
250
|
+
previewTargetRef.current = null
|
|
231
251
|
showToast('Failed to generate preview', 'error')
|
|
232
252
|
return
|
|
233
253
|
}
|
|
@@ -239,7 +259,7 @@ export function MarkdownEditorOverlay() {
|
|
|
239
259
|
setIsPreview(false)
|
|
240
260
|
isMarkdownPreview.value = false
|
|
241
261
|
}
|
|
242
|
-
}, [isPreview, restoreOriginalHTML])
|
|
262
|
+
}, [isPreview, restoreOriginalHTML, findMarkdownWrapper])
|
|
243
263
|
|
|
244
264
|
const handleCancel = useCallback(() => {
|
|
245
265
|
restoreOriginalHTML()
|
|
@@ -326,8 +346,8 @@ export function MarkdownEditorOverlay() {
|
|
|
326
346
|
>
|
|
327
347
|
{/* Header */}
|
|
328
348
|
<div class="flex items-center justify-between px-5 py-4 border-b border-white/10">
|
|
329
|
-
<div class="flex items-center gap-3">
|
|
330
|
-
<div class="flex items-center text-white">
|
|
349
|
+
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
350
|
+
<div class="flex items-center text-white shrink-0">
|
|
331
351
|
<svg
|
|
332
352
|
width="20"
|
|
333
353
|
height="20"
|
|
@@ -345,36 +365,11 @@ export function MarkdownEditorOverlay() {
|
|
|
345
365
|
<line x1="10" y1="9" x2="8" y2="9" />
|
|
346
366
|
</svg>
|
|
347
367
|
</div>
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
value={(page.frontmatter.title as string) || (page.frontmatter.name as string) || ''}
|
|
352
|
-
placeholder={isDataCollection ? 'Entry name...' : 'Page title...'}
|
|
353
|
-
onInput={(e) => {
|
|
354
|
-
const title = (e.target as HTMLInputElement).value
|
|
355
|
-
// Data collections may use 'name' instead of 'title'
|
|
356
|
-
const titleField = isDataCollection && !('title' in page.frontmatter) && 'name' in page.frontmatter ? 'name' : 'title'
|
|
357
|
-
updateMarkdownFrontmatter({ [titleField]: title })
|
|
358
|
-
// Auto-generate slug in create mode if not manually edited
|
|
359
|
-
if (isCreateMode && !slugManuallyEdited) {
|
|
360
|
-
markdownEditorState.value = {
|
|
361
|
-
...markdownEditorState.value,
|
|
362
|
-
currentPage: markdownEditorState.value.currentPage
|
|
363
|
-
? {
|
|
364
|
-
...markdownEditorState.value.currentPage,
|
|
365
|
-
slug: slugify(title),
|
|
366
|
-
isDirty: true,
|
|
367
|
-
}
|
|
368
|
-
: null,
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}}
|
|
372
|
-
class="text-base font-semibold text-white m-0 bg-transparent border-none outline-none placeholder-white/40 w-64"
|
|
373
|
-
data-cms-ui
|
|
374
|
-
/>
|
|
375
|
-
</div>
|
|
368
|
+
<span class="text-base font-semibold text-white truncate">
|
|
369
|
+
{(page.frontmatter.title as string) || (page.frontmatter.name as string) || (isDataCollection ? 'Entry name' : 'Page title')}
|
|
370
|
+
</span>
|
|
376
371
|
</div>
|
|
377
|
-
<div class="flex items-center gap-2">
|
|
372
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
378
373
|
{!isDataCollection && (
|
|
379
374
|
<button
|
|
380
375
|
type="button"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defaultValueCtx, Editor, editorViewCtx, rootCtx } from '@milkdown/core'
|
|
2
2
|
import { listener, listenerCtx } from '@milkdown/plugin-listener'
|
|
3
3
|
import {
|
|
4
4
|
commonmark,
|
|
@@ -15,10 +15,10 @@ import { callCommand, insert, replaceAll } from '@milkdown/utils'
|
|
|
15
15
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
16
16
|
import { uploadMedia } from '../markdown-api'
|
|
17
17
|
import { insertMdxComponentCommand, mdxComponentPlugin } from '../milkdown-mdx-plugin'
|
|
18
|
+
import { type ActiveFormats, defaultActiveFormats, isInListType, setupFormatTracking, toggleHeading } from '../milkdown-utils'
|
|
18
19
|
import { config, mdxComponentPickerOpen, openMediaLibraryWithCallback, resetMarkdownEditorState, showToast, updateMarkdownContent } from '../signals'
|
|
19
20
|
import { MdxComponentIcon } from './mdx-block-view'
|
|
20
21
|
import { MdxComponentPicker } from './mdx-component-picker'
|
|
21
|
-
import { MdxPropsEditor } from './mdx-props-editor'
|
|
22
22
|
|
|
23
23
|
export interface MarkdownInlineEditorProps {
|
|
24
24
|
elementId: string
|
|
@@ -45,27 +45,7 @@ export function MarkdownInlineEditor({
|
|
|
45
45
|
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
|
|
46
46
|
|
|
47
47
|
// Track active formatting for toolbar highlighting
|
|
48
|
-
const [activeFormats, setActiveFormats] = useState<
|
|
49
|
-
bold: boolean
|
|
50
|
-
italic: boolean
|
|
51
|
-
strikethrough: boolean
|
|
52
|
-
link: boolean
|
|
53
|
-
linkHref: string | null
|
|
54
|
-
bulletList: boolean
|
|
55
|
-
orderedList: boolean
|
|
56
|
-
blockquote: boolean
|
|
57
|
-
heading: number | null
|
|
58
|
-
}>({
|
|
59
|
-
bold: false,
|
|
60
|
-
italic: false,
|
|
61
|
-
strikethrough: false,
|
|
62
|
-
link: false,
|
|
63
|
-
linkHref: null,
|
|
64
|
-
bulletList: false,
|
|
65
|
-
orderedList: false,
|
|
66
|
-
blockquote: false,
|
|
67
|
-
heading: null,
|
|
68
|
-
})
|
|
48
|
+
const [activeFormats, setActiveFormats] = useState<ActiveFormats>(defaultActiveFormats)
|
|
69
49
|
|
|
70
50
|
// Store initial content in ref to avoid stale closure issues
|
|
71
51
|
const initialContentRef = useRef(initialContent)
|
|
@@ -79,91 +59,11 @@ export function MarkdownInlineEditor({
|
|
|
79
59
|
const isMdxRef = useRef(isMdx ?? false)
|
|
80
60
|
isMdxRef.current = isMdx ?? false
|
|
81
61
|
|
|
82
|
-
// Check active formatting at current selection
|
|
83
|
-
const updateActiveFormats = useCallback(() => {
|
|
84
|
-
if (!editorInstanceRef.current) return
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
88
|
-
const { state } = view
|
|
89
|
-
const { $from, from, to } = state.selection
|
|
90
|
-
|
|
91
|
-
// Check marks (inline formatting)
|
|
92
|
-
let bold = false
|
|
93
|
-
let italic = false
|
|
94
|
-
let strikethrough = false
|
|
95
|
-
let link = false
|
|
96
|
-
let linkHref: string | null = null
|
|
97
|
-
|
|
98
|
-
// Check if marks are active in the selection
|
|
99
|
-
const marks = state.storedMarks || $from.marks()
|
|
100
|
-
for (const mark of marks) {
|
|
101
|
-
if (mark.type.name === 'strong') bold = true
|
|
102
|
-
if (mark.type.name === 'emphasis') italic = true
|
|
103
|
-
if (mark.type.name === 'strikethrough') strikethrough = true
|
|
104
|
-
if (mark.type.name === 'link') {
|
|
105
|
-
link = true
|
|
106
|
-
linkHref = mark.attrs.href as string
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Also check marks in the selection range
|
|
111
|
-
if (from !== to) {
|
|
112
|
-
state.doc.nodesBetween(from, to, (node) => {
|
|
113
|
-
if (node.marks) {
|
|
114
|
-
for (const mark of node.marks) {
|
|
115
|
-
if (mark.type.name === 'strong') bold = true
|
|
116
|
-
if (mark.type.name === 'emphasis') italic = true
|
|
117
|
-
if (mark.type.name === 'strikethrough') strikethrough = true
|
|
118
|
-
if (mark.type.name === 'link') {
|
|
119
|
-
link = true
|
|
120
|
-
linkHref = mark.attrs.href as string
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Check block types (lists, blockquote, heading)
|
|
128
|
-
let bulletList = false
|
|
129
|
-
let orderedList = false
|
|
130
|
-
let blockquote = false
|
|
131
|
-
let heading: number | null = null
|
|
132
|
-
|
|
133
|
-
for (let depth = $from.depth; depth > 0; depth--) {
|
|
134
|
-
const node = $from.node(depth)
|
|
135
|
-
if (node.type.name === 'bullet_list') bulletList = true
|
|
136
|
-
if (node.type.name === 'ordered_list') orderedList = true
|
|
137
|
-
if (node.type.name === 'blockquote') blockquote = true
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check heading at current position
|
|
141
|
-
const parentNode = $from.parent
|
|
142
|
-
if (parentNode.type.name === 'heading') {
|
|
143
|
-
heading = parentNode.attrs.level as number
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
setActiveFormats({
|
|
147
|
-
bold,
|
|
148
|
-
italic,
|
|
149
|
-
strikethrough,
|
|
150
|
-
link,
|
|
151
|
-
linkHref,
|
|
152
|
-
bulletList,
|
|
153
|
-
orderedList,
|
|
154
|
-
blockquote,
|
|
155
|
-
heading,
|
|
156
|
-
})
|
|
157
|
-
} catch {
|
|
158
|
-
// Ignore errors during format checking
|
|
159
|
-
}
|
|
160
|
-
}, [])
|
|
161
|
-
|
|
162
62
|
// Initialize Milkdown editor
|
|
163
63
|
useEffect(() => {
|
|
164
64
|
if (!editorRef.current) return
|
|
165
65
|
|
|
166
|
-
let
|
|
66
|
+
let cleanupTracking: (() => void) | undefined
|
|
167
67
|
|
|
168
68
|
const initEditor = async () => {
|
|
169
69
|
try {
|
|
@@ -193,20 +93,8 @@ export function MarkdownInlineEditor({
|
|
|
193
93
|
setIsReady(true)
|
|
194
94
|
onEditorReadyRef.current?.(editor)
|
|
195
95
|
|
|
196
|
-
// Set up selection change listener
|
|
197
|
-
|
|
198
|
-
const view = editor.ctx.get(editorViewCtx)
|
|
199
|
-
const originalDispatch = view.dispatch.bind(view)
|
|
200
|
-
view.dispatch = (tr) => {
|
|
201
|
-
originalDispatch(tr)
|
|
202
|
-
if (tr.selectionSet || tr.docChanged) {
|
|
203
|
-
cancelAnimationFrame(formatRaf)
|
|
204
|
-
formatRaf = requestAnimationFrame(updateActiveFormats)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Initial format check
|
|
209
|
-
updateActiveFormats()
|
|
96
|
+
// Set up selection change listener with shallow equality check
|
|
97
|
+
cleanupTracking = setupFormatTracking(editor, setActiveFormats)
|
|
210
98
|
} catch (error) {
|
|
211
99
|
console.error('Milkdown editor initialization failed:', error)
|
|
212
100
|
showToast('Failed to initialize markdown editor', 'error')
|
|
@@ -216,11 +104,11 @@ export function MarkdownInlineEditor({
|
|
|
216
104
|
initEditor()
|
|
217
105
|
|
|
218
106
|
return () => {
|
|
219
|
-
|
|
107
|
+
cleanupTracking?.()
|
|
220
108
|
editorInstanceRef.current?.destroy()
|
|
221
109
|
editorInstanceRef.current = null
|
|
222
110
|
}
|
|
223
|
-
}, [
|
|
111
|
+
}, [])
|
|
224
112
|
|
|
225
113
|
const handleSave = useCallback(() => {
|
|
226
114
|
onSave(content)
|
|
@@ -283,18 +171,12 @@ export function MarkdownInlineEditor({
|
|
|
283
171
|
)
|
|
284
172
|
|
|
285
173
|
// Check if selection is inside a list of given type
|
|
286
|
-
const
|
|
174
|
+
const checkInList = useCallback(
|
|
287
175
|
(listType: 'bullet_list' | 'ordered_list'): boolean => {
|
|
288
176
|
if (!editorInstanceRef.current) return false
|
|
289
177
|
try {
|
|
290
178
|
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
291
|
-
|
|
292
|
-
const { $from } = state.selection
|
|
293
|
-
for (let depth = $from.depth; depth > 0; depth--) {
|
|
294
|
-
const node = $from.node(depth)
|
|
295
|
-
if (node.type.name === listType) return true
|
|
296
|
-
}
|
|
297
|
-
return false
|
|
179
|
+
return isInListType(view, listType)
|
|
298
180
|
} catch {
|
|
299
181
|
return false
|
|
300
182
|
}
|
|
@@ -304,21 +186,21 @@ export function MarkdownInlineEditor({
|
|
|
304
186
|
|
|
305
187
|
// Toggle bullet list - if in bullet list, remove it; otherwise add it
|
|
306
188
|
const handleBulletList = useCallback(() => {
|
|
307
|
-
if (
|
|
189
|
+
if (checkInList('bullet_list')) {
|
|
308
190
|
runCommand(liftListItemCommand.key)
|
|
309
191
|
} else {
|
|
310
192
|
runCommand(wrapInBulletListCommand.key)
|
|
311
193
|
}
|
|
312
|
-
}, [runCommand,
|
|
194
|
+
}, [runCommand, checkInList])
|
|
313
195
|
|
|
314
196
|
// Toggle ordered list - if in ordered list, remove it; otherwise add it
|
|
315
197
|
const handleOrderedList = useCallback(() => {
|
|
316
|
-
if (
|
|
198
|
+
if (checkInList('ordered_list')) {
|
|
317
199
|
runCommand(liftListItemCommand.key)
|
|
318
200
|
} else {
|
|
319
201
|
runCommand(wrapInOrderedListCommand.key)
|
|
320
202
|
}
|
|
321
|
-
}, [runCommand,
|
|
203
|
+
}, [runCommand, checkInList])
|
|
322
204
|
|
|
323
205
|
const handleInsertLink = useCallback(() => {
|
|
324
206
|
if (!editorInstanceRef.current) return
|
|
@@ -369,24 +251,20 @@ export function MarkdownInlineEditor({
|
|
|
369
251
|
}, [activeFormats.link, activeFormats.linkHref])
|
|
370
252
|
|
|
371
253
|
const handleInsertHeading = useCallback((level: number) => {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
editorInstanceRef.current.action(insert(headingMarkdown))
|
|
379
|
-
} catch (error) {
|
|
380
|
-
console.error('Failed to insert heading:', error)
|
|
381
|
-
}
|
|
254
|
+
if (!editorInstanceRef.current) return
|
|
255
|
+
try {
|
|
256
|
+
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
257
|
+
toggleHeading(view, level)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('Failed to toggle heading:', error)
|
|
382
260
|
}
|
|
383
261
|
}, [])
|
|
384
262
|
|
|
385
263
|
// MDX component insertion
|
|
386
|
-
const handleInsertMdxComponent = useCallback((componentName: string, props: Record<string, string
|
|
264
|
+
const handleInsertMdxComponent = useCallback((componentName: string, props: Record<string, string>, children?: string) => {
|
|
387
265
|
if (editorInstanceRef.current) {
|
|
388
266
|
try {
|
|
389
|
-
editorInstanceRef.current.action(callCommand(insertMdxComponentCommand.key, { componentName, props }))
|
|
267
|
+
editorInstanceRef.current.action(callCommand(insertMdxComponentCommand.key, { componentName, props, children }))
|
|
390
268
|
} catch (error) {
|
|
391
269
|
console.error('Failed to insert MDX component:', error)
|
|
392
270
|
}
|
|
@@ -397,42 +275,36 @@ export function MarkdownInlineEditor({
|
|
|
397
275
|
mdxComponentPickerOpen.value = true
|
|
398
276
|
}, [])
|
|
399
277
|
|
|
400
|
-
const handleUpdateMdxProps = useCallback((nodePos: number, props: Record<string, string>) => {
|
|
401
|
-
if (!editorInstanceRef.current) return
|
|
402
|
-
try {
|
|
403
|
-
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
404
|
-
const node = view.state.doc.nodeAt(nodePos)
|
|
405
|
-
if (node && node.type.name === 'mdx_component') {
|
|
406
|
-
const tr = view.state.tr.setNodeMarkup(nodePos, undefined, {
|
|
407
|
-
...node.attrs,
|
|
408
|
-
props: JSON.stringify(props),
|
|
409
|
-
})
|
|
410
|
-
view.dispatch(tr)
|
|
411
|
-
}
|
|
412
|
-
} catch (error) {
|
|
413
|
-
console.error('Failed to update MDX component props:', error)
|
|
414
|
-
}
|
|
415
|
-
}, [])
|
|
416
|
-
|
|
417
278
|
// Drag and drop handlers for direct image upload
|
|
279
|
+
// Only intercept external file drags — let ProseMirror handle internal drags (node reorder)
|
|
280
|
+
const hasFiles = (e: DragEvent) => e.dataTransfer?.types?.includes('Files') ?? false
|
|
281
|
+
|
|
418
282
|
const handleDragOver = useCallback((e: DragEvent) => {
|
|
283
|
+
if (!hasFiles(e)) return
|
|
419
284
|
e.preventDefault()
|
|
420
285
|
e.stopPropagation()
|
|
421
286
|
setIsDragging(true)
|
|
422
287
|
}, [])
|
|
423
288
|
|
|
424
289
|
const handleDragLeave = useCallback((e: DragEvent) => {
|
|
290
|
+
if (!hasFiles(e)) return
|
|
425
291
|
e.preventDefault()
|
|
426
292
|
e.stopPropagation()
|
|
427
293
|
setIsDragging(false)
|
|
428
294
|
}, [])
|
|
429
295
|
|
|
430
296
|
const handleDrop = useCallback(async (e: DragEvent) => {
|
|
297
|
+
// Only handle external file drops — let ProseMirror handle internal drags (e.g. node reorder)
|
|
298
|
+
if (!hasFiles(e)) return
|
|
299
|
+
|
|
431
300
|
e.preventDefault()
|
|
432
301
|
e.stopPropagation()
|
|
433
302
|
setIsDragging(false)
|
|
434
303
|
|
|
435
|
-
const
|
|
304
|
+
const files = e.dataTransfer?.files
|
|
305
|
+
if (!files || files.length === 0) return
|
|
306
|
+
|
|
307
|
+
const file = files[0]
|
|
436
308
|
if (!file || !file.type.startsWith('image/')) {
|
|
437
309
|
showToast('Please drop an image file', 'error')
|
|
438
310
|
return
|
|
@@ -811,9 +683,6 @@ export function MarkdownInlineEditor({
|
|
|
811
683
|
|
|
812
684
|
{/* MDX Component Picker */}
|
|
813
685
|
{isMdx && <MdxComponentPicker onInsert={handleInsertMdxComponent} />}
|
|
814
|
-
|
|
815
|
-
{/* MDX Props Editor */}
|
|
816
|
-
{isMdx && <MdxPropsEditor onUpdateProps={handleUpdateMdxProps} />}
|
|
817
686
|
</div>
|
|
818
687
|
)
|
|
819
688
|
}
|