e105fa2Export to PDF via the browser's print pipelineJeremy Magland 1import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3e0fff3mdshare: markdown editor with the document stored in the URLJeremy Magland 2import CodeMirrorEditor from './CodeMirrorEditor'
3import Preview from './Preview'
4import { encodeDocument, decodeDocument } from './url'
e105fa2Export to PDF via the browser's print pipelineJeremy Magland 5import { documentTitle } from './title'
3e0fff3mdshare: markdown editor with the document stored in the URLJeremy Magland 6import sampleDoc from './sample.md?raw'
7import './App.css'
10function readHash(): string | null {
11 const h = window.location.hash.replace(/^#/, '')
12 if (!h) return null
13 try {
14 return decodeDocument(h)
15 } catch {
16 return null
17 }
18}
20export default function App() {
21 const [text, setText] = useState<string>(() => readHash() ?? sampleDoc)
22 const [urlLength, setUrlLength] = useState<number>(() => window.location.href.length)
23 const [copied, setCopied] = useState(false)
25 // Debounced text -> URL. replaceState (not assignment) so typing doesn't
26 // flood the browser history; lastWrittenRef lets the hashchange listener
27 // tell our own writes apart from external navigation.
28 const lastWrittenRef = useRef<string | null>(null)
29 useEffect(() => {
30 const t = setTimeout(() => {
31 const encoded = text ? encodeDocument(text) : ''
32 lastWrittenRef.current = encoded
33 const url = new URL(window.location.href)
34 url.hash = encoded
35 window.history.replaceState(null, '', url)
36 setUrlLength(url.href.length)
37 }, 300)
38 return () => clearTimeout(t)
39 }, [text])
41 // URL -> text, when the user pastes a different mdshare link into the
42 // address bar or navigates back/forward.
43 useEffect(() => {
44 const onHashChange = () => {
45 const h = window.location.hash.replace(/^#/, '')
46 if (h === lastWrittenRef.current) return
47 const decoded = readHash()
48 if (decoded !== null) setText(decoded)
49 }
50 window.addEventListener('hashchange', onHashChange)
51 return () => window.removeEventListener('hashchange', onHashChange)
52 }, [])
54 const copyLink = useCallback(async () => {
55 // Encode the current text directly so a fast type-then-copy can't race
56 // the debounced URL write.
57 const url = new URL(window.location.href)
58 url.hash = text ? encodeDocument(text) : ''
59 lastWrittenRef.current = url.hash.replace(/^#/, '')
60 window.history.replaceState(null, '', url)
61 setUrlLength(url.href.length)
62 await navigator.clipboard.writeText(url.href)
63 setCopied(true)
64 setTimeout(() => setCopied(false), 1500)
65 }, [text])
e105fa2Export to PDF via the browser's print pipelineJeremy Magland 67 // The first heading names the document, both in the tab and (below) in the
68 // filename the browser suggests for the PDF.
69 const docTitle = useMemo(() => documentTitle(text), [text])
70 useEffect(() => {
71 document.title =
72 docTitle && docTitle !== 'mdshare' ? `${docTitle} \u00b7 mdshare` : 'mdshare'
73 }, [docTitle])
75 // Export is the browser's own print-to-PDF over the rendered pane (see
76 // print.css), so math stays selectable text and diagrams stay vector. The
77 // tab title is what browsers offer as the PDF filename, so drop the
78 // ' \u00b7 mdshare' suffix for the duration of the print.
79 const exportPdf = useCallback(() => {
80 const restore = document.title
81 document.title = docTitle || 'document'
82 const onAfterPrint = () => {
83 document.title = restore
84 window.removeEventListener('afterprint', onAfterPrint)
85 }
86 window.addEventListener('afterprint', onAfterPrint)
87 window.print()
88 }, [docTitle])
3e0fff3mdshare: markdown editor with the document stored in the URLJeremy Magland 90 const newDocument = useCallback(() => {
91 if (text.trim() && text !== sampleDoc) {
92 if (!window.confirm('Discard the current document? (The old link keeps working.)')) return
93 }
94 setText('')
95 }, [text])
97 // Simple draggable splitter
98 const [split, setSplit] = useState(0.5)
99 const mainRef = useRef<HTMLDivElement>(null)
100 const onSplitterDown = useCallback((e: React.PointerEvent) => {
101 e.preventDefault()
102 const main = mainRef.current
103 if (!main) return
104 const rect = main.getBoundingClientRect()
105 const onMove = (ev: PointerEvent) => {
106 const f = (ev.clientX - rect.left) / rect.width
107 setSplit(Math.min(0.8, Math.max(0.2, f)))
108 }
109 const onUp = () => {
110 window.removeEventListener('pointermove', onMove)
111 window.removeEventListener('pointerup', onUp)
112 }
113 window.addEventListener('pointermove', onMove)
114 window.addEventListener('pointerup', onUp)
115 }, [])
117 return (
118 <div className="app">
119 <header className="topbar">
120 <span className="title">mdshare</span>
121 <span className="subtitle">markdown that lives in the URL</span>
122 <span className="spacer" />
123 <span className="url-size" title="Length of the shareable URL">
124 URL: {urlLength.toLocaleString()} chars
125 </span>
126 <button onClick={newDocument}>New</button>
e105fa2Export to PDF via the browser's print pipelineJeremy Magland 127 <button onClick={exportPdf} title="Print or save as PDF (Ctrl/Cmd+P)">
128 Export PDF
129 </button>
3e0fff3mdshare: markdown editor with the document stored in the URLJeremy Magland 130 <button className="primary" onClick={copyLink}>
131 {copied ? 'Copied!' : 'Copy link'}
132 </button>
133 </header>
134 <main className="panes" ref={mainRef}>
135 <section className="pane" style={{ flexBasis: `${split * 100}%` }}>
136 <CodeMirrorEditor value={text} onChange={setText} />
137 </section>
138 <div className="splitter" onPointerDown={onSplitterDown} />
139 <section className="pane pane-preview">
140 <Preview markdown={text} />
141 </section>
142 </main>
143 </div>
144 )
145}