td/source/scrollbar.js
2025-03-14 20:14:27 +01:00

59 lines
1.8 KiB
JavaScript

//
// Inspired by https://github.com/karaggeorge/ink-scrollbar
//
import { Text, Box } from 'ink';
import React, { useState, useEffect, useCallback } from 'react';
export default function Scrollbar({ current, thumbCharacter, highlight, padding, show, children }) {
const length = children.length;
const limit = Math.min(show, length);
const maxLength = Math.max(...children.map(option => option.length));
const [viewpoint, setViewpoint] = useState(
Math.min(Math.max(0, current - Math.floor(limit / 2)), length - limit)
);
useEffect(() => {
if (current < viewpoint) {
setViewpoint(current);
} else if (current >= viewpoint + limit) {
setViewpoint(Math.min(current - limit + 1, length - limit));
}
}, [current, viewpoint, limit, length]);
const getScrollbar = useCallback(() => {
const height = Math.max(Math.round((limit / length) * limit), 1);
if (height === limit) return [];
const pos = Math.min(Math.round((viewpoint / length) * limit), limit - 1);
return Array.from({ length: height }, (_, i) => pos + i);
}, [limit, length, viewpoint]);
const getOption = useCallback((option, index) => {
if (!highlight || current !== index + viewpoint) return option;
const props = highlight === true ? { green: true } : highlight;
return <Box {...props}>{option}</Box>;
}, [current, viewpoint, highlight]);
const getSpacing = useCallback((option) => {
return '\u00A0'.repeat(maxLength - option.length + padding);
}, [maxLength, padding]);
const scrollbar = getScrollbar();
return (
children.filter((_, i) => i >= viewpoint && i < viewpoint + limit)
.map((option, i) => (
<Box key={i}>
<Text>{scrollbar.includes(i) ? `\u00A0${thumbCharacter}` : '\u00A0'.repeat(thumbCharacter.length + 1)}</Text>
{getOption(option, i)}
{getSpacing(option)}
</Box>
))
);
};