Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions packages/app-expo/src/screens/reader/TOCTreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";

import { ChevronDownIcon, ChevronRightIcon } from "@/components/ui/Icon";
import { type ThemeColors, fontSize, fontWeight, radius, useColors } from "@/styles/theme";
import { getFirstTocHref } from "@readany/core/reader";
import type { TOCItem } from "@readany/core/types";

export function TOCTreeItem({
Expand All @@ -21,7 +22,9 @@ export function TOCTreeItem({
}) {
const colors = useColors();
const tocS = makeTocStyles(colors);
const hasChildren = item.subitems && item.subitems.length > 0;
const children = item.subitems ?? [];
const hasChildren = children.length > 0;
const targetHref = getFirstTocHref(item);
const isCurrent = item.title === currentChapter;
const hasCurrentChild = (items: TOCItem[]): boolean => {
for (const child of items) {
Expand All @@ -30,20 +33,28 @@ export function TOCTreeItem({
}
return false;
};
const shouldExpand = hasChildren && hasCurrentChild(item.subitems!);
const shouldExpand = hasChildren && hasCurrentChild(children);
const [expanded, setExpanded] = useState(shouldExpand);
const handlePress = () => {
if (targetHref) {
onSelect(targetHref);
return;
}

if (hasChildren) setExpanded((value) => !value);
};

return (
<View>
<TouchableOpacity
style={[tocS.item, { paddingLeft: 12 + level * 16 }, isCurrent && tocS.itemActive]}
onPress={() => item.href && onSelect(item.href)}
onPress={handlePress}
activeOpacity={0.7}
>
{hasChildren ? (
<TouchableOpacity
style={tocS.expandBtn}
onPress={() => setExpanded(!expanded)}
onPress={() => setExpanded((value) => !value)}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
{expanded ? (
Expand All @@ -61,7 +72,7 @@ export function TOCTreeItem({
</TouchableOpacity>
{expanded && hasChildren && (
<View>
{item.subitems!.map((child) => (
{children.map((child) => (
<TOCTreeItem
key={child.id || child.href}
item={child}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/reader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export { FONT_THEMES, DEFAULT_FONT_THEME, getFontTheme } from "./font-themes";
export { DEFAULT_BINDINGS, isInputElement, matchBinding, findAction } from "./keyboard";
export type { KeyBinding } from "./keyboard";

// Table of contents
export { getFirstTocHref } from "./toc";

// Pagination
export {
getPageDirection,
Expand Down
78 changes: 78 additions & 0 deletions packages/core/src/reader/toc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import { getFirstTocHref } from "./toc";

describe("getFirstTocHref", () => {
it("returns the current item href when present", () => {
expect(
getFirstTocHref({
id: "chapter-1",
title: "Chapter 1",
level: 0,
href: "chapter-1.xhtml",
subitems: [
{
id: "chapter-1-1",
title: "Chapter 1.1",
level: 1,
href: "chapter-1-1.xhtml",
},
],
}),
).toBe("chapter-1.xhtml");
});

it("falls back to the first descendant href for grouping nodes", () => {
expect(
getFirstTocHref({
id: "volume-1",
title: "Volume 1",
level: 0,
subitems: [
{
id: "part-1",
title: "Part 1",
level: 1,
subitems: [
{
id: "chapter-1",
title: "Chapter 1",
level: 2,
href: "text/chapter-1.xhtml#start",
},
],
},
],
}),
).toBe("text/chapter-1.xhtml#start");
});

it("ignores blank href values", () => {
expect(
getFirstTocHref({
id: "volume-1",
title: "Volume 1",
level: 0,
href: " ",
subitems: [
{
id: "chapter-1",
title: "Chapter 1",
level: 1,
href: "chapter-1.xhtml",
},
],
}),
).toBe("chapter-1.xhtml");
});

it("returns null when no item in the branch can be opened", () => {
expect(
getFirstTocHref({
id: "volume-1",
title: "Volume 1",
level: 0,
subitems: [{ id: "part-1", title: "Part 1", level: 1 }],
}),
).toBeNull();
});
});
13 changes: 13 additions & 0 deletions packages/core/src/reader/toc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { TOCItem } from "../types";

export function getFirstTocHref(item: TOCItem | null | undefined): string | null {
const href = item?.href?.trim();
if (href) return href;

for (const child of item?.subitems ?? []) {
const childHref = getFirstTocHref(child);
if (childHref) return childHref;
}

return null;
}