import { type CSSProperties, useContext, useMemo, useEffect, useRef, useState, } from "react"; import h from "../hyper"; import { useNoteLayout } from "./layout"; import { NoteEditorContext } from "./editor"; import type { NoteData } from "./types"; import { NotePositioner, NoteConnector, NodeConnectorOptions, } from "./connector"; import { useColumn } from "../context"; type NoteListProps = NodeConnectorOptions & { onClickNote?: (note: NoteData) => void; }; export function NotesList(props: NoteListProps) { let { onClickNote, ...rest } = props; const { notes, nodes: nodeIndex, updateHeight, noteComponent, scale, } = useNoteLayout(); const { pixelHeight: columnHeight } = useColumn(); const notesInfo = useMemo(() => { let notes1 = notes.map((note) => { const node = nodeIndex[note.id]; const pixelHeight = node?.width ?? 10; const pixelOffset = node?.currentPos ?? scale(note.top_height); return { note, node, pixelOffset, pixelHeight, spacing: { above: pixelOffset - pixelHeight, below: columnHeight - pixelOffset, }, }; }); // Adjust spacing to account for nearby nodes for (let i = 0; i < notes1.length; i++) { const { spacing, pixelOffset } = notes1[i]; if (i > 0) { const prevNote = notes1[i - 1]; // Get distance from the previous note's bottom // to the current note's top spacing.above = pixelOffset - prevNote.pixelOffset - prevNote.pixelHeight; prevNote.spacing.below = spacing.above; } } return notes1; }, [notes, nodeIndex, scale]); return h( "g.notes-list", notesInfo.map(({ note, pixelOffset, pixelHeight, spacing }) => { // If the note has a bad pixelOffset, skip it if (pixelOffset == null || pixelHeight == null) { return null; } return h(Note, { key: note.id, note, pixelOffset, pixelHeight, updateHeight, onClick: onClickNote, noteBodyComponent: noteComponent, spacing, ...rest, }); }), ); } type NodeSpacing = { above: number; below: number; }; interface NoteProps extends NodeConnectorOptions { note: NoteData; style?: object; pixelOffset?: number; pixelHeight?: number; updateHeight?: (id: string | number, height: number) => void; onClick?: (note: NoteData) => void; noteBodyComponent: (props: { note: NoteData; spacing?: NodeSpacing }) => any; spacing?: NodeSpacing; } function Note(props: NoteProps) { const { note, pixelOffset, pixelHeight, updateHeight, deltaConnectorAttachment, connectorOverhang, showPointMarker, noteBodyComponent, spacing, onClick, } = props; const ref = useRef(null); useEffect(() => { if (ref.current) { const newHeight = ref.current.offsetHeight; if (newHeight !== pixelHeight) { updateHeight(note.id, newHeight); } } }, [note, pixelHeight, updateHeight]); const offsetY = pixelOffset; const noteHeight = pixelHeight; const { setEditingNote, editingNote } = useContext(NoteEditorContext) as any; const onClick_ = onClick ?? setEditingNote; const _onClickHandler = useMemo(() => { if (!onClick_) return undefined; return (evt) => { onClick_(note); }; }, [onClick_]); if (editingNote === note) { return null; } // A note can carry its own color — the connector, its endpoint and anything // else keyed on `--note-color` follow it let style: CSSProperties | undefined = undefined; if (note.color != null) { style = { "--note-color": note.color } as CSSProperties; } return h("g.note", { style }, [ h(NoteConnector, { note, deltaConnectorAttachment, connectorOverhang, showPointMarker, }), h( NotePositioner, { offsetY, noteHeight, ref, onClick: _onClickHandler, }, h(noteBodyComponent, { note, spacing }), ), ]); } export { NotePositioner, NoteConnector };