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.

170 lines (145 loc) 4.78 kB
import PropTypes from 'prop-types'; import Backdrop from '@mui/material/Backdrop'; import InsertDriveFileSharpIcon from '@mui/icons-material/InsertDriveFileSharp'; import { grey } from '@mui/material/colors'; import { v4 as uuid } from 'uuid'; import { NativeTypes } from 'react-dnd-html5-backend'; import { useDrop } from 'react-dnd'; import { readImageMetadata } from '../lib/readImageMetadata'; /** */ const safeParseURL = (str) => { try { return new URL(str); } catch (e) { console.warn('Invalid URL:', str); return null; } }; /** */ export const handleDrop = (item, monitor, props) => { const { onDrop } = props; if (item.html) { const doc = new DOMParser().parseFromString(item.html, 'text/html'); // Try to find a link tag const link = doc.querySelector('a[href]'); if (link) { const url = safeParseURL(link.href); if (url) { // Recursively call the method again with the extracted URL return handleDrop({ urls: [url.toString()] }, monitor, props); } } } if (item.urls) { item.urls.forEach((str) => { const url = safeParseURL(str); if (!url) return; const manifestId = url.searchParams.get('manifest'); const canvasId = url.searchParams.get('canvas'); if (manifestId) onDrop({ canvasId, manifestId }, props, monitor); }); } if (item.files) { const manifestFiles = item.files.filter(f => f.type === 'application/json'); const manifestPromises = manifestFiles.map(file => ( new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener('load', () => { const manifestJson = reader.result; const manifestId = uuid(); if (manifestJson) onDrop({ manifestId, manifestJson }, props, monitor); resolve(); }); reader.readAsText(file); }) )); const imageFiles = item.files.filter(({ type }) => type.startsWith('image/')); let imagePromise; if (imageFiles.length > 0) { const id = uuid(); const imageData = imageFiles.map(file => readImageMetadata(file)); imagePromise = Promise.all(imageData).then((images) => { const manifestJson = { '@context': 'http://iiif.io/api/presentation/3/context.json', id, items: images.map(({ name, type, width, height, url, }, index) => ({ height, id: `${id}/canvas/${index}`, items: [ { id: `${id}/canvas/${index}/1`, items: [{ body: { format: type, id: url, type: 'Image', }, height, id: `${id}/canvas/${index}/1/image`, motivation: 'painting', target: `${id}/canvas/${index}/1`, type: 'Annotation', width, }], type: 'AnnotationPage', }, ], label: name, type: 'Canvas', width, })), label: images[0].name, type: 'Manifest', }; const manifestId = uuid(); if (manifestJson) onDrop({ manifestId, manifestJson }, props, monitor); }); } return Promise.all([...manifestPromises, imagePromise]); } return undefined; }; /** */ export const IIIFDropTarget = (props) => { const { children, onDrop } = props; const [{ canDrop, isOver }, drop] = useDrop({ accept: [NativeTypes.URL, NativeTypes.FILE, NativeTypes.HTML], collect: monitor => ({ canDrop: monitor.canDrop(), isOver: monitor.isOver(), }), /** */ drop(item, monitor) { if (!onDrop) return; handleDrop(item, monitor, props); }, }); /** * Safari reports drag+drop'ed urls as both a file and uri-list * which gets mis-classified by react-dnd. */ const hackForSafari = (e) => { if (!window.safari || !onDrop || !e.dataTransfer) return; if (e.dataTransfer.types.includes('Files') && e.dataTransfer.types.includes('text/uri-list')) { const url = e.dataTransfer.getData('text/uri-list'); if (!url) return; handleDrop({ urls: [url] }, null, props); } }; const isActive = canDrop && isOver; return ( <div ref={drop} onDrop={hackForSafari} style={{ height: '100%', width: '100%' }}> {children} <Backdrop open={isActive} style={{ zIndex: 9999 }}> <InsertDriveFileSharpIcon style={{ color: grey[400], fontSize: 256 }} /> </Backdrop> </div> ); }; IIIFDropTarget.propTypes = { children: PropTypes.node.isRequired, onDrop: PropTypes.func.isRequired, };