182 lines
4.6 KiB
TypeScript
182 lines
4.6 KiB
TypeScript
import clsx from 'clsx';
|
|
import React, { useCallback } from 'react';
|
|
import { ListChildComponentProps } from 'react-window';
|
|
import { TOCItem } from '@/libs/document';
|
|
import { getContentMd5 } from '@/utils/misc';
|
|
|
|
const createExpanderIcon = (isExpanded: boolean) => {
|
|
return (
|
|
<svg
|
|
viewBox='0 0 8 10'
|
|
width='8'
|
|
height='10'
|
|
className={clsx(
|
|
'text-base-content transform transition-transform',
|
|
isExpanded ? 'rotate-90' : 'rotate-0',
|
|
)}
|
|
style={{ transformOrigin: 'center' }}
|
|
fill='currentColor'
|
|
>
|
|
<polygon points='0 0, 8 5, 0 10' />
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
export interface FlatTOCItem {
|
|
item: TOCItem;
|
|
depth: number;
|
|
index: number;
|
|
isExpanded?: boolean;
|
|
}
|
|
|
|
const TOCItemView = React.memo<{
|
|
bookKey: string;
|
|
flatItem: FlatTOCItem;
|
|
itemSize?: number;
|
|
isActive: boolean;
|
|
onToggleExpand: (item: TOCItem) => void;
|
|
onItemClick: (item: TOCItem) => void;
|
|
}>(({ flatItem, itemSize, isActive, onToggleExpand, onItemClick }) => {
|
|
const { item, depth } = flatItem;
|
|
|
|
const handleToggleExpand = useCallback(
|
|
(event: React.MouseEvent) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onToggleExpand(item);
|
|
},
|
|
[item, onToggleExpand],
|
|
);
|
|
|
|
const handleClickItem = useCallback(
|
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
|
event.preventDefault();
|
|
onItemClick(item);
|
|
},
|
|
[item, onItemClick],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
tabIndex={0}
|
|
role='treeitem'
|
|
onClick={item.href ? handleClickItem : undefined}
|
|
onKeyDown={item.href ? (e) => e.key === 'Enter' && handleClickItem(e) : undefined}
|
|
aria-expanded={flatItem.isExpanded ? 'true' : 'false'}
|
|
aria-selected={isActive ? 'true' : 'false'}
|
|
data-href={item.href ? getContentMd5(item.href) : undefined}
|
|
className={clsx(
|
|
'flex w-full cursor-pointer items-center rounded-md py-4 sm:py-2',
|
|
isActive
|
|
? 'text-bold-in-eink sm:bg-base-300/65 sm:hover:bg-base-300/75 sm:text-base-content text-blue-500'
|
|
: 'sm:hover:bg-base-300/75',
|
|
)}
|
|
style={{
|
|
height: itemSize ? `${itemSize}px` : 'auto',
|
|
paddingInlineStart: `${(depth + 1) * 12}px`,
|
|
}}
|
|
>
|
|
{item.subitems && (
|
|
<button
|
|
onClick={handleToggleExpand}
|
|
onKeyDown={(e) => {
|
|
e.stopPropagation();
|
|
}}
|
|
className='inline-block cursor-pointer'
|
|
style={{
|
|
padding: '12px',
|
|
margin: '-12px',
|
|
}}
|
|
>
|
|
{createExpanderIcon(flatItem.isExpanded || false)}
|
|
</button>
|
|
)}
|
|
<div
|
|
className='ms-2 truncate text-ellipsis'
|
|
style={{
|
|
maxWidth: 'calc(100% - 24px)',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{item.label}
|
|
</div>
|
|
{(item.location || item.index !== undefined) && (
|
|
<div className='text-base-content/50 ms-auto ps-1 text-xs sm:pe-1'>
|
|
{item.location ? item.location.current + 1 : item.index + 1}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
TOCItemView.displayName = 'TOCItemView';
|
|
|
|
interface ListRowProps {
|
|
bookKey: string;
|
|
flatItem: FlatTOCItem;
|
|
itemSize?: number;
|
|
activeHref: string | null;
|
|
onToggleExpand: (item: TOCItem) => void;
|
|
onItemClick: (item: TOCItem) => void;
|
|
}
|
|
|
|
export const StaticListRow: React.FC<ListRowProps> = ({
|
|
bookKey,
|
|
flatItem,
|
|
itemSize,
|
|
activeHref,
|
|
onToggleExpand,
|
|
onItemClick,
|
|
}) => {
|
|
const isActive = activeHref === flatItem.item.href;
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
'border-base-300 w-full border-b sm:border-none',
|
|
'pe-4 ps-2 pt-[1px] sm:pe-2',
|
|
)}
|
|
title={flatItem.item.label || ''}
|
|
>
|
|
<TOCItemView
|
|
bookKey={bookKey}
|
|
flatItem={flatItem}
|
|
itemSize={itemSize}
|
|
isActive={isActive}
|
|
onToggleExpand={onToggleExpand}
|
|
onItemClick={onItemClick}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const VirtualListRow: React.FC<
|
|
ListChildComponentProps & {
|
|
data: {
|
|
bookKey: string;
|
|
flatItems: FlatTOCItem[];
|
|
itemSize: number;
|
|
activeHref: string | null;
|
|
onToggleExpand: (item: TOCItem) => void;
|
|
onItemClick: (item: TOCItem) => void;
|
|
};
|
|
}
|
|
> = ({ index, style, data }) => {
|
|
const { flatItems, bookKey, activeHref, itemSize, onToggleExpand, onItemClick } = data;
|
|
const flatItem = flatItems[index];
|
|
|
|
return (
|
|
<div style={style} title={flatItem.item.label || ''}>
|
|
<StaticListRow
|
|
bookKey={bookKey}
|
|
flatItem={flatItem}
|
|
itemSize={itemSize - 1}
|
|
activeHref={activeHref}
|
|
onToggleExpand={onToggleExpand}
|
|
onItemClick={onItemClick}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|