src/components/EditorLayout.jsx (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
import { useDeferredValue, useEffect, useMemo, useState } from 'react';
import { MODES } from '../lib/editorConstants.js';
import { buildPreviewHtml, ensureMarkedLoaded } from '../lib/previewHtml.js';
function useDividerOrientation() {
const [orientation, setOrientation] = useState('vertical');
useEffect(() => {
const media = window.matchMedia('(max-width: 768px)');
const update = () => setOrientation(media.matches ? 'horizontal' : 'vertical');
update();
media.addEventListener('change', update);
return () => media.removeEventListener('change', update);
}, []);
return orientation;
}
export function countWords(text) {
const trimmed = text.trim();
if (!trimmed) return 0;
return trimmed.split(/\s+/).filter(Boolean).length;
}
export default function EditorLayout({
mode,
content,
onContentChange,
readOnly = false,
showEditor = true,
editorRef,
previewOnly = false,
}) {
const [markedReady, setMarkedReady] = useState(false);
const dividerOrientation = useDividerOrientation();
const deferredContent = useDeferredValue(content);
const previewIsStale = content !== deferredContent;
useEffect(() => {
if (mode !== MODES.MARKDOWN) return;
let cancelled = false;
ensureMarkedLoaded().then(() => {
if (!cancelled) setMarkedReady(true);
});
return () => {
cancelled = true;
};
}, [mode]);
const html = useMemo(() => {
return buildPreviewHtml(mode, deferredContent);
}, [mode, deferredContent, markedReady]);
const wordCount = useMemo(() => countWords(content), [content]);
const charCount = content.length;
const editorAriaLabel =
mode === MODES.ORG ? 'Org-mode editing area' : 'Markdown editing area';
return (
<>
<main className={`app-layout${previewOnly ? ' app-layout--preview-only' : ''}`}>
{showEditor && !previewOnly && (
<>
<section
className="panel panel-editor"
aria-label={mode === MODES.ORG ? 'Org-mode editor' : 'Markdown editor'}
>
<div className="panel-label">Write</div>
<textarea
ref={editorRef}
className="editor"
value={content}
onChange={onContentChange ? (e) => onContentChange(e.target.value) : undefined}
readOnly={readOnly}
spellCheck="true"
aria-label={editorAriaLabel}
placeholder="Start writing..."
/>
</section>
<div
className="layout-divider"
role="separator"
aria-orientation={dividerOrientation}
/>
</>
)}
<section
className={`panel panel-preview${previewOnly ? ' panel-preview--full' : ''}`}
aria-label="Document preview"
>
<div className="panel-label">Read</div>
<div
className={`preview-paper${previewIsStale ? ' preview-updating' : ''}`}
dangerouslySetInnerHTML={{ __html: html }}
/>
</section>
</main>
<div className="app-footer-stats app-footer-stats--inline">
<span>
{wordCount} {wordCount === 1 ? 'word' : 'words'}
</span>
<span className="footer-separator">ยท</span>
<span>
{charCount} {charCount === 1 ? 'character' : 'characters'}
</span>
</div>
</>
);
}
|