editor.chain().focus().toggleBold().run()}>
B editor.chain().focus().toggleItalic().run()}>
ILink editor.chain().focus().toggleBulletList().run()}
>
• List
editor.chain().focus().toggleBlockquote().run()}
>
❝ Quote
{onAddPhoto && (
+ Flickr photo
)}
{/* Typing @ opens the list of things to link to, so this just types it. */}
editor.chain().focus().insertContent('@').run()}>
@ Link to…
);
}
const search = createLinkSearch();
function suggestionState(props) {
return {items: props.items, selected: 0, rect: props.clientRect && props.clientRect(), command: props.command};
}
// A rich text editor for a post body that stores markdown, so people can
// format without knowing it. The Markdown tab shows and edits the raw text.
// The markdown goes in a hidden field called `name`, so the surrounding form
// submits it as it would a textarea.
//
// With `autosave` (see useDraftAutosave) the post is saved as a draft as it is
// written. With `previewUrl` a Preview tab shows the body as the site will
// render it.
//
// With `canAddPhoto`, the toolbar picks a Flickr photo for the post and places
// it in the text. `photos` are the post's photos so far ({id, title,
// thumbnail_url, fullsize_url}), `postPath` is where an existing post lives
// (a draft not yet saved has none: it is saved first), and `cardPhotoId` is the
// photo that stands for the post.
export default function PostEditor({
name, value = '', id = 'post_body', autosave = null, previewUrl = null,
canAddPhoto = false, photos = [], postPath = null, cardPhotoId = null,
}) {
const [markdown, setMarkdown] = useState(value);
const [tab, setTab] = useState('write');
const [previewHtml, setPreviewHtml] = useState('');
const {status, saveNow} = useDraftAutosave(markdown, autosave);
const [notice, setNotice] = useState('');
const [pickerUrl, setPickerUrl] = useState(null); // the add photo dialog's address, while it is open
// The photos and card photo the figures in the text look up as they draw.
const photoList = useRef(photos);
const cardId = useRef(cardPhotoId);
const editorRef = useRef(null);
// Where the post lives, saving the draft first if it is a new one. Null when
// there is nothing to save yet (it needs a title and some text).
async function ensurePostPath() {
return autosave ? saveNow() : postPath;
}
async function makeCardPhoto(photoId) {
const path = await ensurePostPath();
if (!path) return false;
const {ok} = await patchJson(path, {post: {card_photo_id: photoId}});
if (ok) cardId.current = photoId;
return ok;
}
async function openPhotoPicker() {
setNotice('');
const path = await ensurePostPath();
if (!path) {
setNotice('The draft could not be saved, so a photo cannot be added yet. Try again.');
return;
}
setPickerUrl(`/photos/new?type=post&id=${path.split('/').pop()}`);
}
function photoAdded({photo}) {
setPickerUrl(null);
photoList.current = [...photoList.current, photo];
// After the current line, or after the photo that is selected: inserting
// over a selected photo would replace it, and only one could ever be added.
const editor = editorRef.current;
const {selection} = editor.state;
const position = selection.node ? selection.to : selection.from;
editor
.chain()
.focus()
.insertContentAt(position, {type: 'photoFigure', attrs: {photoId: photo.id, caption: photo.title, size: 'wide'}})
.run();
}
const figure = useMemo(() => photoFigure.configure({
getPhoto: (photoId) => photoList.current.find((p) => p.id === photoId),
getCardPhotoId: () => cardId.current,
setCardPhoto: (photoId) => makeCardPhoto(photoId),
}), []); // eslint-disable-line react-hooks/exhaustive-deps
const tabs = [['write', 'Write'], previewUrl && ['preview', 'Preview'], ['markdown', 'Markdown']].filter(Boolean);
async function loadPreview() {
setPreviewHtml('');
const response = await fetch(previewUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken()},
body: JSON.stringify({body: markdown}),
});
setPreviewHtml(response.ok ? await response.text() : '
{tab === 'write' && editor && (
<>
{suggest && (
)}
>
)}
{tab === 'preview' && (
// The server renders (and sanitises) the markdown, as it does for a posted post.
)}
{tab === 'markdown' && (