Personal no-bs blogging platform.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127import { setBlockType, toggleMark, wrapIn } from "prosemirror-commands"import type { EditorView } from "prosemirror-view";import type { Command, EditorState, Transaction } from "prosemirror-state";import { Plugin } from "prosemirror-state";import { schema } from "prosemirror-markdown";import { publish_post, update_post, last_update, delete_post } from "./sync";
interface MenuButtonOptions { dom: HTMLSpanElement command?: Command}
function icon(text: string, name: string, styles?: string) { let button = document.createElement("button"); button.className = "menuicon " + name; button.title = name; button.innerHTML = text; styles ? button.style = styles : ""; return button;}
function heading(level: number) { return { command: setBlockType(schema.nodes.heading!, { level }), dom: icon("H" + level, "heading") }}
class MenuView { items: MenuButtonOptions[]; editorView: EditorView; dom: HTMLDivElement; constructor(items: MenuButtonOptions[], editorView: EditorView) { this.items = items; this.editorView = editorView;
this.dom = document.createElement("div"); items.forEach((item) => this.dom.appendChild(item.dom)) this.dom.className = "editor-toolbar"; this.dom.id = "editor-toolbar";
this.dom.addEventListener("mousedown", e => { e.preventDefault() editorView.focus() items.forEach(({ command, dom }) => { if (!command) return; if (dom.contains(e.target as any)) command(editorView.state, editorView.dispatch, editorView); }) }) }
update() { this.items.forEach(({ command, dom }) => { if (!command || dom.classList.contains("save") || dom.classList.contains("publish")) return; let active = command(this.editorView.state, undefined, this.editorView); dom.style.display = active ? "" : "none"; }) }
destroy() { this.dom.remove() }}
export function menuPlugin(items: MenuButtonOptions[]) { return new Plugin({ view(editorView) { let menuView = new MenuView(items, editorView); editorView.dom.parentNode!.insertBefore(menuView.dom, editorView.dom); return menuView; } })}
const save_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-floppy2" viewBox="0 0 16 16"> <path d="M1.5 0h11.586a1.5 1.5 0 0 1 1.06.44l1.415 1.414A1.5 1.5 0 0 1 16 2.914V14.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13A1.5 1.5 0 0 1 1.5 0M1 1.5v13a.5.5 0 0 0 .5.5H2v-4.5A1.5 1.5 0 0 1 3.5 9h9a1.5 1.5 0 0 1 1.5 1.5V15h.5a.5.5 0 0 0 .5-.5V2.914a.5.5 0 0 0-.146-.353l-1.415-1.415A.5.5 0 0 0 13.086 1H13v3.5A1.5 1.5 0 0 1 11.5 6h-7A1.5 1.5 0 0 1 3 4.5V1H1.5a.5.5 0 0 0-.5.5m9.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5z"/></svg>`;
const publish_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe-americas-fill" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="m8 0 .412.01A7.97 7.97 0 0 1 13.29 2a8.04 8.04 0 0 1 2.548 4.382 8 8 0 1 1-15.674 0 8 8 0 0 1 1.361-3.078A8 8 0 0 1 2.711 2 7.96 7.96 0 0 1 8 0m0 1a7 7 0 0 0-5.958 3.324C2.497 6.192 6.669 7.827 6.5 8c-.5.5-1.034.884-1 1.5.07 1.248 2.259.774 2.5 2 .202 1.032-1.051 3 0 3 1.5-.5 3.798-3.186 4-5 .138-1.242-2-2-3.5-2.5-.828-.276-1.055.648-1.5.5S4.5 5.5 5.5 5s1 0 1.5.5c1 .5.5-1 1-1.5.838-.838 3.16-1.394 3.605-2.001A6.97 6.97 0 0 0 8 1"/></svg>`;
const delete_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/> <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/></svg>`
function setPublishLabel(dom: HTMLButtonElement) { const isPublished = !!last_update.published_at; dom.innerHTML = (isPublished ? "Unpublish " : "Publish ") + publish_icon;}
const publishButton = icon("", "publish");setPublishLabel(publishButton);
const publishItem = { dom: publishButton, command: async (_state: EditorState, _dispatch: Transaction, view: EditorView) => { await publish_post(); setPublishLabel(publishButton); return true; }};
export const menu = menuPlugin([ { command: toggleMark(schema.marks.strong!), dom: icon("<strong>B</strong>", "strong") }, { command: toggleMark(schema.marks.em!), dom: icon("<em>i</em>", "em") }, { command: setBlockType(schema.nodes.paragraph!), dom: icon("¶", "paragraph") }, heading(1), heading(2), heading(3), { command: wrapIn(schema.nodes.blockquote!), dom: icon(">", "blockquote") }, { dom: icon("Save " + save_icon, "save", "margin-left: auto;"), command: (state, dispatch, view) => { if (dispatch && view) update_post(view.state); return true; // don't hide } }, publishItem as any, { dom: icon("Delete " + delete_icon, "delete", "color: var(--warning);"), command: async (state, dispatch, view) => { if (dispatch && view) await delete_post(); return true; // don't hide } },]);
setPublishLabel(publishButton);