fixde: editor breaking when clicking vim button
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { EditorState, Compartment } from '@codemirror/state';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { languages } from '@codemirror/language-data';
|
import { languages } from '@codemirror/language-data';
|
||||||
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
||||||
@@ -18,7 +18,6 @@ interface Props {
|
|||||||
editSlug?: string;
|
editSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeMirror theme matching the Catppuccin narlblog style
|
|
||||||
const narlblogTheme = EditorView.theme({
|
const narlblogTheme = EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
backgroundColor: 'var(--crust)',
|
backgroundColor: 'var(--crust)',
|
||||||
@@ -42,7 +41,6 @@ const narlblogTheme = EditorView.theme({
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
},
|
},
|
||||||
'.cm-activeLineGutter': { backgroundColor: 'var(--surface0)' },
|
'.cm-activeLineGutter': { backgroundColor: 'var(--surface0)' },
|
||||||
// Search panel styling
|
|
||||||
'.cm-panels': {
|
'.cm-panels': {
|
||||||
backgroundColor: 'var(--mantle)',
|
backgroundColor: 'var(--mantle)',
|
||||||
color: 'var(--text)',
|
color: 'var(--text)',
|
||||||
@@ -50,7 +48,6 @@ const narlblogTheme = EditorView.theme({
|
|||||||
},
|
},
|
||||||
'.cm-searchMatch': { backgroundColor: 'var(--yellow)', opacity: '0.3' },
|
'.cm-searchMatch': { backgroundColor: 'var(--yellow)', opacity: '0.3' },
|
||||||
'.cm-searchMatch-selected': { backgroundColor: 'var(--peach)', opacity: '0.4' },
|
'.cm-searchMatch-selected': { backgroundColor: 'var(--peach)', opacity: '0.4' },
|
||||||
// Vim cursor styling
|
|
||||||
'.cm-fat-cursor': {
|
'.cm-fat-cursor': {
|
||||||
backgroundColor: 'var(--mauve) !important',
|
backgroundColor: 'var(--mauve) !important',
|
||||||
color: 'var(--crust) !important',
|
color: 'var(--crust) !important',
|
||||||
@@ -98,6 +95,9 @@ function renderMathInElement(element: HTMLElement) {
|
|||||||
for (let i = 0; i < 3; i++) walk(element);
|
for (let i = 0; i < 3; i++) walk(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compartment for hot-swapping vim mode without recreating the editor
|
||||||
|
const vimCompartment = new Compartment();
|
||||||
|
|
||||||
export default function Editor({ editSlug }: Props) {
|
export default function Editor({ editSlug }: Props) {
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
const viewRef = useRef<EditorView | null>(null);
|
const viewRef = useRef<EditorView | null>(null);
|
||||||
@@ -131,7 +131,6 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
result.then(h => { if (previewRef.current) previewRef.current.innerHTML = h; });
|
result.then(h => { if (previewRef.current) previewRef.current.innerHTML = h; });
|
||||||
}
|
}
|
||||||
// Enhance preview after render
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!previewRef.current) return;
|
if (!previewRef.current) return;
|
||||||
renderMathInElement(previewRef.current);
|
renderMathInElement(previewRef.current);
|
||||||
@@ -141,52 +140,50 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
});
|
});
|
||||||
}, [showPreview]);
|
}, [showPreview]);
|
||||||
|
|
||||||
function buildExtensions() {
|
// Initialize CodeMirror once
|
||||||
const exts = [
|
|
||||||
...(vimEnabled ? [vim()] : []),
|
|
||||||
keymap.of([...defaultKeymap, ...searchKeymap, indentWithTab]),
|
|
||||||
search(),
|
|
||||||
closeBrackets(),
|
|
||||||
markdown({ base: markdownLanguage, codeLanguages: languages }),
|
|
||||||
EditorView.lineWrapping,
|
|
||||||
narlblogTheme,
|
|
||||||
cmPlaceholder('# Hello World\nWrite your markdown here...'),
|
|
||||||
EditorView.updateListener.of(update => {
|
|
||||||
if (!update.docChanged) return;
|
|
||||||
// Debounced preview update
|
|
||||||
if (previewTimerRef.current) clearTimeout(previewTimerRef.current);
|
|
||||||
previewTimerRef.current = setTimeout(updatePreview, 300);
|
|
||||||
// Autocomplete trigger
|
|
||||||
const pos = update.state.selection.main.head;
|
|
||||||
const line = update.state.doc.lineAt(pos);
|
|
||||||
const textBefore = line.text.slice(0, pos - line.from);
|
|
||||||
const lastChar = textBefore.slice(-1);
|
|
||||||
if (lastChar === '/' || lastChar === '!') {
|
|
||||||
triggerAutocomplete(update.view);
|
|
||||||
} else if (lastChar === ' ' || textBefore.length === 0) {
|
|
||||||
setShowAutocomplete(false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
return exts;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize / rebuild CodeMirror when vim mode toggles
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorRef.current) return;
|
if (!editorRef.current || viewRef.current) return;
|
||||||
|
|
||||||
// Preserve content if rebuilding
|
|
||||||
const oldContent = viewRef.current?.state.doc.toString() || '';
|
|
||||||
if (viewRef.current) viewRef.current.destroy();
|
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: oldContent,
|
doc: '',
|
||||||
extensions: buildExtensions(),
|
extensions: [
|
||||||
|
vimCompartment.of(window.innerWidth > 768 ? vim() : []),
|
||||||
|
keymap.of([...defaultKeymap, ...searchKeymap, indentWithTab]),
|
||||||
|
search(),
|
||||||
|
closeBrackets(),
|
||||||
|
markdown({ base: markdownLanguage, codeLanguages: languages }),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
narlblogTheme,
|
||||||
|
cmPlaceholder('# Hello World\nWrite your markdown here...'),
|
||||||
|
EditorView.updateListener.of(update => {
|
||||||
|
if (!update.docChanged) return;
|
||||||
|
if (previewTimerRef.current) clearTimeout(previewTimerRef.current);
|
||||||
|
previewTimerRef.current = setTimeout(updatePreview, 300);
|
||||||
|
const pos = update.state.selection.main.head;
|
||||||
|
const line = update.state.doc.lineAt(pos);
|
||||||
|
const textBefore = line.text.slice(0, pos - line.from);
|
||||||
|
const lastChar = textBefore.slice(-1);
|
||||||
|
if (lastChar === '/' || lastChar === '!') {
|
||||||
|
triggerAutocomplete(update.view);
|
||||||
|
} else if (lastChar === ' ' || textBefore.length === 0) {
|
||||||
|
setShowAutocomplete(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const view = new EditorView({ state, parent: editorRef.current });
|
const view = new EditorView({ state, parent: editorRef.current });
|
||||||
viewRef.current = view;
|
viewRef.current = view;
|
||||||
|
|
||||||
return () => { view.destroy(); viewRef.current = null; };
|
return () => { view.destroy(); viewRef.current = null; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Hot-swap vim mode via compartment reconfiguration
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewRef.current) return;
|
||||||
|
viewRef.current.dispatch({
|
||||||
|
effects: vimCompartment.reconfigure(vimEnabled ? vim() : []),
|
||||||
|
});
|
||||||
}, [vimEnabled]);
|
}, [vimEnabled]);
|
||||||
|
|
||||||
// Load existing post for editing
|
// Load existing post for editing
|
||||||
@@ -202,7 +199,6 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
}).catch(() => showAlertMsg('Failed to load post.', 'error'));
|
}).catch(() => showAlertMsg('Failed to load post.', 'error'));
|
||||||
}, [editSlug]);
|
}, [editSlug]);
|
||||||
|
|
||||||
// Update preview when toggled on
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showPreview) updatePreview();
|
if (showPreview) updatePreview();
|
||||||
}, [showPreview, updatePreview]);
|
}, [showPreview, updatePreview]);
|
||||||
@@ -389,10 +385,10 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor + Preview */}
|
{/* Editor + Preview — both columns stretch to the taller one */}
|
||||||
<div className={`relative ${showPreview ? 'grid grid-cols-1 md:grid-cols-2 gap-4' : ''}`}>
|
<div className={`relative ${showPreview ? 'grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch' : ''}`}>
|
||||||
<div className="relative">
|
<div className="relative min-h-[500px]" style={showPreview ? { display: 'flex', flexDirection: 'column' } : undefined}>
|
||||||
<div ref={editorRef} className="min-h-[500px]" />
|
<div ref={editorRef} style={showPreview ? { flex: '1 1 0', minHeight: 0 } : undefined} className={showPreview ? '' : 'min-h-[500px]'} />
|
||||||
|
|
||||||
{/* Autocomplete dropdown */}
|
{/* Autocomplete dropdown */}
|
||||||
{showAutocomplete && autocompleteAssets.length > 0 && (
|
{showAutocomplete && autocompleteAssets.length > 0 && (
|
||||||
@@ -427,13 +423,13 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live Preview */}
|
{/* Live Preview — stretches to match editor height */}
|
||||||
{showPreview && (
|
{showPreview && (
|
||||||
<div className="border border-surface1 rounded-xl bg-crust/50 overflow-y-auto max-h-[600px] min-h-[500px]">
|
<div className="border border-surface1 rounded-xl bg-crust/50 overflow-y-auto flex flex-col min-h-[500px]">
|
||||||
<div className="sticky top-0 bg-mantle px-4 py-2 text-xs text-subtext0 uppercase border-b border-surface1 z-10">
|
<div className="sticky top-0 bg-mantle px-4 py-2 text-xs text-subtext0 uppercase border-b border-surface1 z-10">
|
||||||
Preview
|
Preview
|
||||||
</div>
|
</div>
|
||||||
<div ref={previewRef} className="prose max-w-none p-4 md:p-6" />
|
<div ref={previewRef} className="prose max-w-none p-4 md:p-6 flex-1" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const isAdmin = Astro.cookies.has('admin_token');
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="post-content" class="prose max-w-none" set:html={html} />
|
<div id="post-content" class="prose max-w-none" set:html={html} />
|
||||||
<PostEnhancer client:load containerId="post-content" />
|
<PostEnhancer client:only="react" containerId="post-content" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Reference in New Issue
Block a user