NoteEditor.tsx

4.4 KB · raw

1import { onMount } from "solid-js";23export function toHtml(raw: string): string {4  if (/<\/?[a-z]/i.test(raw)) return raw;5  return raw6    .replace(/&/g, "&amp;")7    .replace(/</g, "&lt;")8    .replace(/>/g, "&gt;")9    .replace(/\n/g, "<br>");10}1112function Toolbar(props: { exec: (cmd: string, val?: string) => void }) {13  const e = props.exec;14  return (15    <div class="peach-note-toolbar">16      <button class="peach-note-tool-btn peach-note-tool-bold"17        onMouseDown={(ev) => { ev.preventDefault(); e("bold"); }}18        title="Bold (⌘B)">B</button>19      <button class="peach-note-tool-btn peach-note-tool-italic"20        onMouseDown={(ev) => { ev.preventDefault(); e("italic"); }}21        title="Italic (⌘I)">I</button>22      <div class="peach-note-tool-divider" />23      <button class="peach-note-tool-btn"24        onMouseDown={(ev) => { ev.preventDefault(); e("fontSize", "1"); }}25        title="Small">sm</button>26      <button class="peach-note-tool-btn"27        onMouseDown={(ev) => { ev.preventDefault(); e("fontSize", "3"); }}28        title="Normal">md</button>29      <button class="peach-note-tool-btn"30        onMouseDown={(ev) => { ev.preventDefault(); e("fontSize", "5"); }}31        title="Large">lg</button>32    </div>33  );34}3536/* ── Full-popup overlay editor (Entry.tsx — editing existing notes) ── */3738interface NoteEditorProps {39  initialContent: string;40  saving: boolean;41  onSave: (html: string) => Promise<void>;42  onCancel: () => void;43}4445export function NoteEditor(props: NoteEditorProps) {46  let editorRef: HTMLDivElement | undefined;4748  onMount(() => {49    if (!editorRef) return;50    editorRef.innerHTML = toHtml(props.initialContent);51    editorRef.focus();52    const sel = window.getSelection();53    if (sel) {54      const range = document.createRange();55      range.selectNodeContents(editorRef);56      range.collapse(false);57      sel.removeAllRanges();58      sel.addRange(range);59    }60  });6162  function exec(cmd: string, value?: string) {63    document.execCommand(cmd, false, value);64    editorRef?.focus();65  }6667  function handleKeyDown(e: KeyboardEvent) {68    const mod = e.metaKey || e.ctrlKey;69    if (mod && e.key === "b") { e.preventDefault(); exec("bold"); return; }70    if (mod && e.key === "i") { e.preventDefault(); exec("italic"); return; }71    if (e.key === "Escape") { e.preventDefault(); props.onCancel(); }72  }7374  return (75    <div class="peach-note-editor">76      <div class="peach-note-editor-header">77        <button class="peach-note-editor-back" onClick={props.onCancel}78          disabled={props.saving} aria-label="Cancel">79          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor"80            stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">81            <path d="M6 1L1 6L6 11" />82          </svg>83        </button>84        <span class="peach-note-editor-label">note</span>85        <button class="peach-note-editor-save-btn"86          onClick={() => void props.onSave(editorRef?.innerHTML ?? "")}87          disabled={props.saving}>88          {props.saving ? "saving…" : "save"}89        </button>90      </div>9192      <Toolbar exec={exec} />9394      <div ref={editorRef} class="peach-note-editor-body"95        contenteditable="true" onKeyDown={handleKeyDown}96        aria-multiline="true" aria-label="Note content" />97    </div>98  );99}100101/* ── Inline editor (Add.tsx — embedded in the new-entry form) ── */102103interface NoteEditorInlineProps {104  initialContent: string;105  onContentChange: (html: string) => void;106}107108export function NoteEditorInline(props: NoteEditorInlineProps) {109  let editorRef: HTMLDivElement | undefined;110111  onMount(() => {112    if (!editorRef) return;113    editorRef.innerHTML = toHtml(props.initialContent);114    editorRef.focus();115  });116117  function exec(cmd: string, value?: string) {118    document.execCommand(cmd, false, value);119    editorRef?.focus();120  }121122  function handleKeyDown(e: KeyboardEvent) {123    const mod = e.metaKey || e.ctrlKey;124    if (mod && e.key === "b") { e.preventDefault(); exec("bold"); }125    if (mod && e.key === "i") { e.preventDefault(); exec("italic"); }126  }127128  return (129    <div class="peach-note-editor-inline">130      <Toolbar exec={exec} />131      <div ref={editorRef}132        class="peach-note-editor-body peach-note-editor-body--inline"133        contenteditable="true"134        onKeyDown={handleKeyDown}135        onInput={() => props.onContentChange(editorRef?.innerHTML ?? "")}136        aria-multiline="true"137        aria-label="Note content"138      />139    </div>140  );141}