File
Blob: src/client/components/editor/lib/heading-outline.ts
| 1 | import type { Editor } from "@tiptap/react"; |
| 2 | import { normalizeOutlineText, readOutlineLevel, type OutlineItem } from "@/shared/editor/model/outline"; |
| 3 | |
| 4 | export interface HeadingOutlineItem extends OutlineItem { |
| 5 | pos: number; |
| 6 | } |
| 7 | |
| 8 | export function collectHeadings(editor: Editor | null | undefined): HeadingOutlineItem[] { |
| 9 | if (!editor) return []; |
| 10 | |
| 11 | const headings: HeadingOutlineItem[] = []; |
| 12 | editor.state.doc.descendants((node, pos) => { |
| 13 | if (node.type.name !== "heading") return; |
| 14 | |
| 15 | const text = normalizeOutlineText(node.textContent); |
| 16 | if (!text) return; |
| 17 | |
| 18 | headings.push({ |
| 19 | id: `${pos}`, |
| 20 | text, |
| 21 | level: readOutlineLevel(node.attrs.level), |
| 22 | pos, |
| 23 | }); |
| 24 | }); |
| 25 | |
| 26 | return headings; |
| 27 | } |
| 28 | |
| 29 | export function resolveActiveBySelection(headings: HeadingOutlineItem[], selectionPos: number): number | null { |
| 30 | let active: number | null = null; |
| 31 | for (const heading of headings) { |
| 32 | if (heading.pos <= selectionPos) active = heading.pos; |
| 33 | else break; |
| 34 | } |
| 35 | return active; |
| 36 | } |
| 37 | |
| 38 | export function resolveSelectionHeading(editor: Editor | null | undefined): number | null { |
| 39 | if (!editor) return null; |
| 40 | const { $from } = editor.state.selection; |
| 41 | for (let depth = $from.depth; depth >= 0; depth -= 1) { |
| 42 | if ($from.node(depth).type.name === "heading") { |
| 43 | return $from.before(depth); |
| 44 | } |
| 45 | } |
| 46 | return null; |
| 47 | } |
| 48 | |
| 49 | export function jumpToHeading(editor: Editor, pos: number) { |
| 50 | const targetPos = pos + 1; |
| 51 | |
| 52 | if (editor.isEditable) { |
| 53 | editor.chain().focus(null, { scrollIntoView: false }).setTextSelection(targetPos).run(); |
| 54 | } else { |
| 55 | editor.commands.setTextSelection(targetPos); |
| 56 | } |
| 57 | |
| 58 | const node = editor.view.nodeDOM(pos); |
| 59 | if (node instanceof Element) { |
| 60 | node.scrollIntoView({ block: "center", behavior: "smooth" }); |
| 61 | return; |
| 62 | } |
| 63 | |
| 64 | editor.commands.scrollIntoView(); |
| 65 | } |