Repository navigation
Expand file tree
/
Copy pathuseNoteMarks.ts
More file actions
70 lines (64 loc) · 3.58 KB
/
Copy pathuseNoteMarks.ts
File metadata and controls
70 lines (64 loc) · 3.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { useEffect, useState } from "react";
import { HIGHLIGHT_COLORS } from "../../shared/types.ts";
import type { Chapter, HighlightNote, Note, QuestionNote } from "../../shared/types.ts";
import { readableBlocks } from "./book.ts";
import { useMountVersion } from "./mountRegistry.ts";
import { highlightMark, quoteSpans, rangeInBlock, setHighlight } from "./textRanges.ts";
/** The note a pointer or focus event is about: the card it happened in, if any. */
function noteOf(target: EventTarget | null): string | null {
return target instanceof Element ? (target.closest<HTMLElement>("[data-note]")?.dataset.note ?? null) : null;
}
/** The ranges a note's passage covers in the chapters on the page: none while its chapter is not there. */
function rangesOf(note: Note, chapters: Chapter[]): Range[] {
const chapter = chapters.find((item) => item.id === note.chapterId);
const offset = note.mode === "highlight" ? note.offset : undefined;
const spans = chapter ? quoteSpans(readableBlocks(chapter), note.blockId, note.quote, offset) : [];
return spans.map(({ blockId, span }) => rangeInBlock(blockId, span)).filter((range) => range !== null);
}
/**
* Keeps the passage each note explains marked in the text while the note exists, so the reader can see which
* words a card belongs to. Pointing at a card, or moving focus into it, marks its own passage more strongly.
* Highlights are painted in their own colours, each under a mark of its own.
*/
export function useNoteMarks(notes: QuestionNote[], highlights: HighlightNote[], chapters: Chapter[]): void {
const mountVersion = useMountVersion();
const [hovered, setHovered] = useState<string | null>(null);
const [focused, setFocused] = useState<string | null>(null);
useEffect(() => {
const onOver = (event: PointerEvent) => setHovered(noteOf(event.target));
const onOut = (event: PointerEvent) => setHovered(noteOf(event.relatedTarget));
const onFocusIn = (event: FocusEvent) => setFocused(noteOf(event.target));
const onFocusOut = (event: FocusEvent) => setFocused(noteOf(event.relatedTarget));
document.addEventListener("pointerover", onOver);
document.addEventListener("pointerout", onOut);
document.addEventListener("focusin", onFocusIn);
document.addEventListener("focusout", onFocusOut);
return () => {
document.removeEventListener("pointerover", onOver);
document.removeEventListener("pointerout", onOut);
document.removeEventListener("focusin", onFocusIn);
document.removeEventListener("focusout", onFocusOut);
};
}, []);
useEffect(() => {
const marks = notes.map((note) => ({ id: note.id, ranges: rangesOf(note, chapters) }));
const active = hovered ?? focused;
setHighlight("dr-note", marks.flatMap((mark) => mark.ranges));
setHighlight("dr-note-active", marks.find((mark) => mark.id === active)?.ranges ?? null);
return () => {
setHighlight("dr-note", null);
setHighlight("dr-note-active", null);
};
}, [notes, chapters, hovered, focused, mountVersion]);
// Apart from the note marks, so pointing at a card does not paint every highlight again. Painted again whenever a
// chapter or chunk comes onto the page or goes, so a highlight in it shows.
useEffect(() => {
for (const color of HIGHLIGHT_COLORS) {
const ranges = highlights.filter((highlight) => highlight.color === color).flatMap((highlight) => rangesOf(highlight, chapters));
setHighlight(highlightMark(color), ranges);
}
return () => {
for (const color of HIGHLIGHT_COLORS) setHighlight(highlightMark(color), null);
};
}, [highlights, chapters, mountVersion]);
}