UNPKG

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
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, };