mirador
Version:
An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.
232 lines (217 loc) • 6.67 kB
JSX
import { useCallback, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import Paper from '@mui/material/Paper';
import { List, Grid } from 'react-window';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useCanvasWorldService } from '../hooks';
import ThumbnailCanvasGrouping from '../containers/ThumbnailCanvasGrouping';
import ns from '../config/css-ns';
/**
*/
export function ThumbnailNavigation({
canvasGroupings,
canvasIndex,
hasNextCanvas = false,
hasPreviousCanvas = false,
position,
setNextCanvas = () => {},
setPreviousCanvas = () => {},
thumbnailNavigation,
view = undefined,
viewingDirection = '',
windowId,
}) {
const { t } = useTranslation();
const scrollbarSize = 15;
const spacing = 12; // 2 * (2px margin + 2px border + 2px padding + 2px padding)
const gridRef = useRef();
const previousView = useRef(view);
const canvasWorlds = useCanvasWorldService();
useEffect(() => {
if (previousView.current !== view && position !== 'off') {
previousView.current = view;
// Note: resetAfterIndex is not available in react-window v2 List API
// The list will re-render automatically when rowHeight function changes
}
}, [view, position]);
useEffect(() => {
let index = canvasIndex;
if (view === 'book') index = Math.ceil(index / 2);
// Only scroll if the index is valid
if (gridRef.current && index >= 0 && index < canvasGroupings.length) {
// For horizontal Grid, scroll to column; for vertical List, scroll to row
if (position === 'far-bottom') {
// react-window Grid API in v2
gridRef.current.scrollToCell({
columnIndex: index,
rowIndex: 0,
columnAlign: 'center',
rowAlign: 'center',
});
} else {
// react-window List API in v2
gridRef.current.scrollToRow({ index, align: 'center' });
}
}
}, [canvasIndex, view, canvasGroupings.length, position]);
// Prevent loss of focus when navigating thumbnails
const paperRef = useRef(null);
useEffect(() => {
if (paperRef.current && document.activeElement !== paperRef.current) {
paperRef.current.focus();
}
}, [canvasIndex]);
/** */
const handleKeyDown = (e) => {
let nextKey = 'ArrowRight';
let previousKey = 'ArrowLeft';
if (position === 'far-right') {
nextKey = 'ArrowDown';
previousKey = 'ArrowUp';
}
switch (e.key) {
case nextKey:
nextCanvas();
break;
case previousKey:
previousCanvas();
break;
default:
break;
}
};
/**
* When on right, row height
* When on bottom, column width
*/
const calculateScaledSize = (index) => {
const canvases = canvasGroupings[index];
if (!canvases) return thumbnailNavigation.width + spacing;
const world = canvasWorlds.get(canvases);
const bounds = world.worldBounds();
switch (position) {
case 'far-right': {
const calc = Math.floor((calculatingWidth(canvases.length) * bounds[3]) / bounds[2]);
if (!Number.isInteger(calc)) return thumbnailNavigation.width + spacing;
return calc + spacing;
}
// Default case bottom
default: {
if (bounds[3] === 0) return thumbnailNavigation.width + spacing;
const calc = Math.ceil(((thumbnailNavigation.height - scrollbarSize - spacing - 4) * bounds[2]) / bounds[3]);
return calc;
}
}
};
/** */
const calculatingWidth = (canvasesLength) => {
if (canvasesLength === 1) {
return thumbnailNavigation.width;
}
return thumbnailNavigation.width * 2;
};
/** */
const style = useCallback(() => {
const width = view === 'book' ? thumbnailNavigation.width * 2 : thumbnailNavigation.width;
switch (position) {
case 'far-right':
return {
height: '100%',
minHeight: 0,
width: `${width + scrollbarSize + spacing}px`,
};
// Default case bottom
default:
return {
height: `${thumbnailNavigation.height}px`,
width: '100%',
};
}
}, [position, thumbnailNavigation, view]);
/**
*/
const nextCanvas = () => {
if (hasNextCanvas) setNextCanvas();
};
/**
*/
const previousCanvas = () => {
if (hasPreviousCanvas) setPreviousCanvas();
};
if (position === 'off') {
return null;
}
const htmlDir = viewingDirection === 'right-to-left' ? 'rtl' : 'ltr';
const rowData = {
canvasGroupings,
height: thumbnailNavigation.height - spacing - scrollbarSize,
position,
windowId,
};
return (
<Paper
className={classNames(ns('thumb-navigation'))}
sx={{
'&:focus': {
boxShadow: 0,
outline: 0,
},
}}
aria-label={t('thumbnailNavigation')}
square
elevation={0}
style={style()}
tabIndex={0}
onKeyDown={handleKeyDown}
ref={paperRef}
>
<div style={{ height: '100%', width: '100%' }}>
{canvasGroupings.length > 0 && position === 'far-bottom' && (
<Grid
columnCount={canvasGroupings.length}
columnWidth={calculateScaledSize}
rowCount={1}
rowHeight={() => thumbnailNavigation.height - spacing - scrollbarSize}
height={thumbnailNavigation.height}
width="100%"
style={{
direction: htmlDir === 'rtl' ? 'rtl' : 'ltr',
}}
cellProps={rowData}
gridRef={gridRef}
cellComponent={ThumbnailCanvasGrouping}
/>
)}
{canvasGroupings.length > 0 && position === 'far-right' && (
<List
defaultHeight={100}
rowCount={canvasGroupings.length}
rowHeight={calculateScaledSize}
style={{
direction: htmlDir === 'rtl' ? 'rtl' : 'ltr',
height: '100%',
width: '100%',
}}
rowProps={rowData}
listRef={gridRef}
rowComponent={ThumbnailCanvasGrouping}
/>
)}
</div>
</Paper>
);
}
ThumbnailNavigation.propTypes = {
canvasGroupings: PropTypes.array.isRequired,
canvasIndex: PropTypes.number.isRequired,
hasNextCanvas: PropTypes.bool,
hasPreviousCanvas: PropTypes.bool,
position: PropTypes.string.isRequired,
setNextCanvas: PropTypes.func,
setPreviousCanvas: PropTypes.func,
thumbnailNavigation: PropTypes.object.isRequired,
view: PropTypes.string,
viewingDirection: PropTypes.string,
windowId: PropTypes.string.isRequired,
};