Let a margin remark become a note, and lead to it ever after
Some remarks turn into a page of their own. "Notiz anlegen" on a margin card makes one, titles it from what was written, and goes there; the pin keeps its id so the passage leads to that note afterwards. The note is created with a link back to the document, so the connection reads both ways and the document lists it among its backlinks — a note born from a passage should not be a note with nothing on it saying which passage. An existing note can be linked instead, and a link can be let go of without touching the note. Both are offered on the phone sheet too. Two older bugs stood in the way and are fixed: `openLinkTarget` finds a note's notebook by looking it up in the notes list, and a note created in the same tick is not in the copy that callback closed over — so jumping to a fresh note went nowhere at all. Creating one now navigates by the notebook it already knows. A drawing save waits 700ms for the next stroke, and leaving the document inside that window dropped it. That is precisely what turning a remark into a note does, so the link would have been lost on the way out. The viewer flushes a pending write when it unmounts, which also covers drawing and navigating away quickly. 215 unit tests, 229 browser checks across eleven suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
011ba82a94
commit
502aecfc92
10 files changed
+541
-12
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { PinLinkPicker } from "./PinLinkPicker.jsx";
|
||||
|
||||
/**
|
||||
* The notes for a page, written in the margin beside it.
|
||||
*
|
||||
@@ -19,6 +21,9 @@ export const MARGIN_W = 208;
|
||||
const CARD_H = 74;
|
||||
const CARD_GAP = 6;
|
||||
|
||||
/** A card carrying a link to a note is one row taller. */
|
||||
const heightOf = (note) => CARD_H + (note.noteId ? 20 : 0);
|
||||
|
||||
/** Cards at their anchors, pushed down where they would sit on top of each other. */
|
||||
function layout(notes, pageHeight, cssHeight) {
|
||||
let previousBottom = -Infinity;
|
||||
@@ -28,13 +33,14 @@ function layout(notes, pageHeight, cssHeight) {
|
||||
.map((note) => {
|
||||
const anchor = (note.y / pageHeight) * cssHeight - 12;
|
||||
const top = Math.max(anchor, previousBottom + CARD_GAP);
|
||||
previousBottom = top + CARD_H;
|
||||
previousBottom = top + heightOf(note);
|
||||
return { note, top };
|
||||
});
|
||||
}
|
||||
|
||||
function Card({ T, note, top, number, open, onOpen, onChange, onRemove }) {
|
||||
function Card({ T, note, top, number, open, onOpen, onChange, onRemove, link }) {
|
||||
const [draft, setDraft] = useState(note.text);
|
||||
const [picking, setPicking] = useState(false);
|
||||
const areaRef = useRef(null);
|
||||
|
||||
// Adopt text that arrived from elsewhere — another device, or an undo — but
|
||||
@@ -138,6 +144,23 @@ function Card({ T, note, top, number, open, onOpen, onChange, onRemove }) {
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
{!note.noteId && (
|
||||
<>
|
||||
<button
|
||||
data-pin-make-note
|
||||
onClick={() => {
|
||||
commit();
|
||||
link.onCreate(note.id, draft);
|
||||
}}
|
||||
style={linkAction(T)}
|
||||
>
|
||||
Notiz anlegen
|
||||
</button>
|
||||
<button data-pin-link onClick={() => setPicking((v) => !v)} style={linkAction(T)}>
|
||||
Verknüpfen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<span style={{ flex: 1 }} />
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -158,6 +181,18 @@ function Card({ T, note, top, number, open, onOpen, onChange, onRemove }) {
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
{picking && (
|
||||
<PinLinkPicker
|
||||
T={T}
|
||||
search={link.search}
|
||||
onClose={() => setPicking(false)}
|
||||
onPick={(target) => {
|
||||
setPicking(false);
|
||||
commit();
|
||||
link.onLink(note.id, target.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
@@ -176,15 +211,86 @@ function Card({ T, note, top, number, open, onOpen, onChange, onRemove }) {
|
||||
{note.text || "Leer — zum Schreiben antippen"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* The note this remark grew into. Shown in both states: when the card
|
||||
is closed it is the whole point of the card, and when it is open it
|
||||
is what stops a second note being made for the same passage. */}
|
||||
{note.noteId && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 6 }}>
|
||||
<button
|
||||
data-pin-goto={note.noteId}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
link.onOpenNote(note.noteId);
|
||||
}}
|
||||
title="Zur Notiz springen"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: T.accent,
|
||||
cursor: "pointer",
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
padding: 0,
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<span style={{ flexShrink: 0 }}>→</span>
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{link.labelOf(note.noteId) ?? "Notiz gelöscht"}
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<button
|
||||
data-pin-unlink
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
link.onLink(note.id, null);
|
||||
}}
|
||||
title="Verweis lösen (die Notiz bleibt)"
|
||||
style={{ ...linkAction(T), color: T.muted }}
|
||||
>
|
||||
Lösen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PdfMargin({ T, notes, pageHeight, cssHeight, openId, onOpen, onChange, onRemove, numberOf }) {
|
||||
const linkAction = (T) => ({
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: T.accent,
|
||||
cursor: "pointer",
|
||||
fontSize: 10.5,
|
||||
padding: 0,
|
||||
marginRight: 10,
|
||||
});
|
||||
|
||||
export function PdfMargin({
|
||||
T,
|
||||
notes,
|
||||
pageHeight,
|
||||
cssHeight,
|
||||
openId,
|
||||
onOpen,
|
||||
onChange,
|
||||
onRemove,
|
||||
numberOf,
|
||||
link,
|
||||
}) {
|
||||
const placed = layout(notes, pageHeight, cssHeight);
|
||||
// Cards near the foot of a page may need more room than the page is tall.
|
||||
const needed = placed.length ? placed[placed.length - 1].top + CARD_H : 0;
|
||||
const last = placed[placed.length - 1];
|
||||
const needed = last ? last.top + heightOf(last.note) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -207,6 +313,7 @@ export function PdfMargin({ T, notes, pageHeight, cssHeight, openId, onOpen, onC
|
||||
onOpen={onOpen}
|
||||
onChange={(text) => onChange(note.id, text)}
|
||||
onRemove={() => onRemove(note.id)}
|
||||
link={link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user