Compare commits
No commits in common. '0c862b978537f7d3f030697faa773b37b288fc45' and 'baf37ef071571896d1d1465af1793574e29194df' have entirely different histories.
0c862b9785
...
baf37ef071
10 changed files with 160 additions and 966 deletions
@ -1,592 +0,0 @@ |
|||||||
import React, { useEffect, useMemo, useState } from 'react'; |
|
||||||
import { |
|
||||||
ConnectedCompanionWindow as CompanionWindow, |
|
||||||
OSDReferences, |
|
||||||
selectAnnotation as selectAnnotationAction, |
|
||||||
setCanvas as setCanvasAction, |
|
||||||
updateWindow as updateWindowAction, |
|
||||||
} from 'mirador'; |
|
||||||
import Box from '@mui/material/Box'; |
|
||||||
import CircularProgress from '@mui/material/CircularProgress'; |
|
||||||
import Divider from '@mui/material/Divider'; |
|
||||||
import List from '@mui/material/List'; |
|
||||||
import ListItemButton from '@mui/material/ListItemButton'; |
|
||||||
import ListItemText from '@mui/material/ListItemText'; |
|
||||||
import TextField from '@mui/material/TextField'; |
|
||||||
import Typography from '@mui/material/Typography'; |
|
||||||
import SvgIcon from '@mui/material/SvgIcon'; |
|
||||||
|
|
||||||
const PANEL_KEY = 'allAnnotations'; |
|
||||||
|
|
||||||
function AllAnnotationsIcon(props) { |
|
||||||
return ( |
|
||||||
<SvgIcon {...props} viewBox="0 0 24 24"> |
|
||||||
<path d="M4 5h3v3H4V5zm5 0h11v3H9V5zM4 10.5h3v3H4v-3zm5 0h11v3H9v-3zM4 16h3v3H4v-3zm5 0h11v3H9v-3z" /> |
|
||||||
</SvgIcon> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function getLabel(label, fallback) { |
|
||||||
if (!label) return fallback; |
|
||||||
if (typeof label === 'string') return label; |
|
||||||
if (Array.isArray(label)) return label.join(', '); |
|
||||||
if (label.en?.length) return label.en.join(', '); |
|
||||||
const first = Object.values(label).find((value) => Array.isArray(value) && value.length); |
|
||||||
return first ? first.join(', ') : fallback; |
|
||||||
} |
|
||||||
|
|
||||||
function canvasesFromManifest(manifest) { |
|
||||||
if (Array.isArray(manifest?.items)) { |
|
||||||
return manifest.items |
|
||||||
.filter((item) => item?.type === 'Canvas' || item?.id) |
|
||||||
.map((canvas, index) => ({ |
|
||||||
id: canvas.id, |
|
||||||
label: getLabel(canvas.label, `Page ${index + 1}`), |
|
||||||
index, |
|
||||||
width: canvas.width, |
|
||||||
height: canvas.height, |
|
||||||
})) |
|
||||||
.filter((canvas) => canvas.id); |
|
||||||
} |
|
||||||
|
|
||||||
const canvases = manifest?.sequences?.[0]?.canvases || []; |
|
||||||
return canvases.map((canvas, index) => ({ |
|
||||||
id: canvas['@id'] || canvas.id, |
|
||||||
label: getLabel(canvas.label, `Page ${index + 1}`), |
|
||||||
index, |
|
||||||
width: canvas.width, |
|
||||||
height: canvas.height, |
|
||||||
})).filter((canvas) => canvas.id); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
function annotationTargetCanvasId(annotation) { |
|
||||||
const target = annotation?.target; |
|
||||||
|
|
||||||
if (typeof target === 'string') { |
|
||||||
return target.split('#')[0] || null; |
|
||||||
} |
|
||||||
|
|
||||||
const source = target?.source; |
|
||||||
if (typeof source === 'string') return source.split('#')[0] || null; |
|
||||||
if (source?.id) return String(source.id).split('#')[0] || null; |
|
||||||
if (source?.['@id']) return String(source['@id']).split('#')[0] || null; |
|
||||||
|
|
||||||
if (target?.id) return String(target.id).split('#')[0] || null; |
|
||||||
if (target?.['@id']) return String(target['@id']).split('#')[0] || null; |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
function selectorObjects(annotation) { |
|
||||||
const target = annotation?.target; |
|
||||||
if (!target || typeof target === 'string') return []; |
|
||||||
if (Array.isArray(target.selector)) return target.selector; |
|
||||||
return target.selector ? [target.selector] : []; |
|
||||||
} |
|
||||||
|
|
||||||
function svgSelectorValue(annotation) { |
|
||||||
const selector = selectorObjects(annotation).find((item) => |
|
||||||
item?.type === 'SvgSelector' || item?.['@type'] === 'oa:SvgSelector'); |
|
||||||
return typeof selector?.value === 'string' ? selector.value : ''; |
|
||||||
} |
|
||||||
|
|
||||||
function svgBounds(annotation) { |
|
||||||
const value = svgSelectorValue(annotation); |
|
||||||
if (!value || typeof DOMParser === 'undefined' || !document?.body) return null; |
|
||||||
|
|
||||||
let host; |
|
||||||
try { |
|
||||||
const parsed = new DOMParser().parseFromString(value, 'image/svg+xml'); |
|
||||||
const parsedSvg = parsed.documentElement; |
|
||||||
if (!parsedSvg || parsedSvg.nodeName.toLowerCase() !== 'svg') return null; |
|
||||||
|
|
||||||
host = document.createElement('div'); |
|
||||||
host.setAttribute('aria-hidden', 'true'); |
|
||||||
host.style.cssText = [ |
|
||||||
'position:absolute', |
|
||||||
'left:-100000px', |
|
||||||
'top:-100000px', |
|
||||||
'width:1px', |
|
||||||
'height:1px', |
|
||||||
'visibility:hidden', |
|
||||||
'overflow:hidden', |
|
||||||
'pointer-events:none', |
|
||||||
].join(';'); |
|
||||||
|
|
||||||
const svg = document.importNode(parsedSvg, true); |
|
||||||
host.appendChild(svg); |
|
||||||
document.body.appendChild(host); |
|
||||||
|
|
||||||
const drawable = svg.querySelectorAll('path,rect,circle,ellipse,polygon,polyline,line'); |
|
||||||
let minX = Infinity; |
|
||||||
let minY = Infinity; |
|
||||||
let maxX = -Infinity; |
|
||||||
let maxY = -Infinity; |
|
||||||
|
|
||||||
drawable.forEach((node) => { |
|
||||||
if (typeof node.getBBox !== 'function') return; |
|
||||||
try { |
|
||||||
const box = node.getBBox(); |
|
||||||
if (![box.x, box.y, box.width, box.height].every(Number.isFinite)) return; |
|
||||||
if (box.width <= 0 && box.height <= 0) return; |
|
||||||
minX = Math.min(minX, box.x); |
|
||||||
minY = Math.min(minY, box.y); |
|
||||||
maxX = Math.max(maxX, box.x + box.width); |
|
||||||
maxY = Math.max(maxY, box.y + box.height); |
|
||||||
} |
|
||||||
catch (_error) { |
|
||||||
// Ignore an individual SVG element that cannot report a bounding box. |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
if (![minX, minY, maxX, maxY].every(Number.isFinite) || maxX <= minX || maxY <= minY) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
x: minX, |
|
||||||
y: minY, |
|
||||||
width: maxX - minX, |
|
||||||
height: maxY - minY, |
|
||||||
}; |
|
||||||
} |
|
||||||
catch (error) { |
|
||||||
console.warn('Unable to read Mirador SVG annotation bounds.', error); |
|
||||||
return null; |
|
||||||
} |
|
||||||
finally { |
|
||||||
if (host?.parentNode) host.parentNode.removeChild(host); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function selectorValue(annotation) { |
|
||||||
const target = annotation?.target; |
|
||||||
|
|
||||||
if (typeof target === 'string') { |
|
||||||
const hash = target.indexOf('#'); |
|
||||||
return hash >= 0 ? target.slice(hash + 1) : ''; |
|
||||||
} |
|
||||||
|
|
||||||
const selectors = Array.isArray(target?.selector) |
|
||||||
? target.selector |
|
||||||
: target?.selector |
|
||||||
? [target.selector] |
|
||||||
: []; |
|
||||||
|
|
||||||
for (const selector of selectors) { |
|
||||||
if (typeof selector === 'string' && selector.includes('xywh=')) { |
|
||||||
return selector; |
|
||||||
} |
|
||||||
if (typeof selector?.value === 'string' && selector.value.includes('xywh=')) { |
|
||||||
return selector.value; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ''; |
|
||||||
} |
|
||||||
|
|
||||||
function annotationBounds(annotation, canvas) { |
|
||||||
const value = selectorValue(annotation); |
|
||||||
const match = value.match(/(?:^|[#&])?xywh=(?:(pixel|percent):)?([0-9.]+),([0-9.]+),([0-9.]+),([0-9.]+)/i); |
|
||||||
|
|
||||||
let bounds = null; |
|
||||||
if (match) { |
|
||||||
let [, unit, x, y, width, height] = match; |
|
||||||
x = Number(x); |
|
||||||
y = Number(y); |
|
||||||
width = Number(width); |
|
||||||
height = Number(height); |
|
||||||
|
|
||||||
if ([x, y, width, height].every(Number.isFinite) && width > 0 && height > 0) { |
|
||||||
if (unit?.toLowerCase() === 'percent') { |
|
||||||
if (canvas?.width && canvas?.height) { |
|
||||||
x = (x / 100) * canvas.width; |
|
||||||
y = (y / 100) * canvas.height; |
|
||||||
width = (width / 100) * canvas.width; |
|
||||||
height = (height / 100) * canvas.height; |
|
||||||
} |
|
||||||
else { |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
bounds = { x, y, width, height }; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// MAE commonly saves drawn regions as an SvgSelector and, in some cases, |
|
||||||
// the accompanying FragmentSelector contains only the canvas URI. Use the |
|
||||||
// SVG geometry as the zoom target when no xywh fragment is available. |
|
||||||
if (!bounds) bounds = svgBounds(annotation); |
|
||||||
if (!bounds) return null; |
|
||||||
|
|
||||||
const paddingX = bounds.width * 0.15; |
|
||||||
const paddingY = bounds.height * 0.15; |
|
||||||
|
|
||||||
return { |
|
||||||
x: Math.max(0, bounds.x - paddingX), |
|
||||||
y: Math.max(0, bounds.y - paddingY), |
|
||||||
width: bounds.width + (paddingX * 2), |
|
||||||
height: bounds.height + (paddingY * 2), |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
function viewerMatchesCanvas(tiledImage, canvas) { |
|
||||||
if (!tiledImage || !canvas?.width || !canvas?.height) return Boolean(tiledImage); |
|
||||||
|
|
||||||
try { |
|
||||||
const size = tiledImage.getContentSize?.(); |
|
||||||
if (!size?.x || !size?.y) return false; |
|
||||||
|
|
||||||
// IIIF/OpenSeadragon dimensions should normally be exact, but allow a |
|
||||||
// tiny tolerance for implementations that round image dimensions. |
|
||||||
const widthMatches = Math.abs(size.x - canvas.width) <= 2; |
|
||||||
const heightMatches = Math.abs(size.y - canvas.height) <= 2; |
|
||||||
return widthMatches && heightMatches; |
|
||||||
} |
|
||||||
catch (_error) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function focusAnnotationWhenViewerReady( |
|
||||||
windowId, |
|
||||||
annotation, |
|
||||||
canvas, |
|
||||||
selectAnnotation, |
|
||||||
updateWindow, |
|
||||||
attempt = 0, |
|
||||||
) { |
|
||||||
const bounds = annotationBounds(annotation, canvas); |
|
||||||
const osdRef = OSDReferences.get(windowId); |
|
||||||
const viewer = osdRef?.current; |
|
||||||
const tiledImage = viewer?.world?.getItemAt?.(0); |
|
||||||
|
|
||||||
const ready = viewer?.viewport |
|
||||||
&& tiledImage |
|
||||||
&& viewerMatchesCanvas(tiledImage, canvas); |
|
||||||
|
|
||||||
if (!ready) { |
|
||||||
if (attempt < 50) { |
|
||||||
window.setTimeout( |
|
||||||
() => focusAnnotationWhenViewerReady( |
|
||||||
windowId, |
|
||||||
annotation, |
|
||||||
canvas, |
|
||||||
selectAnnotation, |
|
||||||
updateWindow, |
|
||||||
attempt + 1, |
|
||||||
), |
|
||||||
100, |
|
||||||
); |
|
||||||
} |
|
||||||
else { |
|
||||||
console.warn('Timed out waiting for Mirador/OpenSeadragon to display the target canvas.'); |
|
||||||
} |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// Canvas changes can complete after the first Redux selection. Reassert the |
|
||||||
// annotation state only after the new OSD image/overlay is actually ready. |
|
||||||
updateWindow(windowId, { highlightAllAnnotations: false }); |
|
||||||
selectAnnotation(windowId, annotation.id); |
|
||||||
|
|
||||||
const applyFocus = () => { |
|
||||||
const currentRef = OSDReferences.get(windowId); |
|
||||||
const currentViewer = currentRef?.current; |
|
||||||
const currentImage = currentViewer?.world?.getItemAt?.(0); |
|
||||||
if (!currentViewer?.viewport || !currentImage) return; |
|
||||||
|
|
||||||
// Reassert selection after OSD has had a chance to replace the old canvas. |
|
||||||
updateWindow(windowId, { highlightAllAnnotations: false }); |
|
||||||
selectAnnotation(windowId, annotation.id); |
|
||||||
currentViewer.forceRedraw?.(); |
|
||||||
|
|
||||||
if (!bounds) return; |
|
||||||
|
|
||||||
try { |
|
||||||
const rect = currentImage.imageToViewportRectangle( |
|
||||||
bounds.x, |
|
||||||
bounds.y, |
|
||||||
bounds.width, |
|
||||||
bounds.height, |
|
||||||
); |
|
||||||
currentViewer.viewport.fitBounds(rect, false); |
|
||||||
currentViewer.forceRedraw?.(); |
|
||||||
} |
|
||||||
catch (error) { |
|
||||||
console.warn('Unable to zoom to Mirador annotation.', error); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
// The Redux canvas changes before OSD necessarily finishes replacing its world item. |
|
||||||
// Apply immediately, then retry a few times. The later passes are intentionally harmless |
|
||||||
// and ensure a single click works even when consecutive canvases have identical dimensions. |
|
||||||
window.requestAnimationFrame(applyFocus); |
|
||||||
[250, 600, 1100].forEach((delay) => window.setTimeout(applyFocus, delay)); |
|
||||||
} |
|
||||||
|
|
||||||
function bodyText(body) { |
|
||||||
const bodies = Array.isArray(body) ? body : body ? [body] : []; |
|
||||||
for (const item of bodies) { |
|
||||||
const value = item?.value ?? item?.chars ?? item?.label; |
|
||||||
if (typeof value === 'string' && value.trim()) { |
|
||||||
const tmp = document.createElement('div'); |
|
||||||
tmp.innerHTML = value; |
|
||||||
return (tmp.textContent || tmp.innerText || value).trim(); |
|
||||||
} |
|
||||||
} |
|
||||||
return ''; |
|
||||||
} |
|
||||||
|
|
||||||
function annotationText(annotation) { |
|
||||||
return bodyText(annotation?.body) |
|
||||||
|| annotation?.label |
|
||||||
|| annotation?.motivation |
|
||||||
|| 'Annotation'; |
|
||||||
} |
|
||||||
|
|
||||||
function AllAnnotationsPanel({ |
|
||||||
id, |
|
||||||
windowId, |
|
||||||
manifestId, |
|
||||||
endpoint, |
|
||||||
selectedCanvasId, |
|
||||||
selectedAnnotationId, |
|
||||||
setCanvas, |
|
||||||
selectAnnotation, |
|
||||||
updateWindow, |
|
||||||
}) { |
|
||||||
const [groups, setGroups] = useState([]); |
|
||||||
const [loading, setLoading] = useState(true); |
|
||||||
const [error, setError] = useState(''); |
|
||||||
const [filter, setFilter] = useState(''); |
|
||||||
const [pendingFocus, setPendingFocus] = useState(null); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
let cancelled = false; |
|
||||||
|
|
||||||
async function load() { |
|
||||||
if (!manifestId || !endpoint) { |
|
||||||
setGroups([]); |
|
||||||
setLoading(false); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
setLoading(true); |
|
||||||
setError(''); |
|
||||||
|
|
||||||
try { |
|
||||||
const manifestResponse = await fetch(manifestId, { |
|
||||||
credentials: 'same-origin', |
|
||||||
headers: { Accept: 'application/json, application/ld+json' }, |
|
||||||
}); |
|
||||||
if (!manifestResponse.ok) { |
|
||||||
throw new Error(`Unable to load manifest (${manifestResponse.status})`); |
|
||||||
} |
|
||||||
|
|
||||||
const manifest = await manifestResponse.json(); |
|
||||||
const canvases = canvasesFromManifest(manifest); |
|
||||||
|
|
||||||
const response = await fetch(endpoint, { |
|
||||||
method: 'POST', |
|
||||||
credentials: 'same-origin', |
|
||||||
headers: { |
|
||||||
Accept: 'application/json', |
|
||||||
'Content-Type': 'application/json', |
|
||||||
}, |
|
||||||
body: JSON.stringify({ canvasIds: canvases.map((canvas) => canvas.id) }), |
|
||||||
}); |
|
||||||
if (!response.ok) { |
|
||||||
const detail = await response.text().catch(() => ''); |
|
||||||
throw new Error(`Unable to load all annotations (${response.status})${detail ? `: ${detail}` : ''}`); |
|
||||||
} |
|
||||||
|
|
||||||
const payload = await response.json(); |
|
||||||
const byCanvas = new Map(); |
|
||||||
const manifestCanvasIds = new Set(canvases.map((canvas) => canvas.id)); |
|
||||||
|
|
||||||
(payload.items || []).forEach((item) => { |
|
||||||
// The Web Annotation target is authoritative. This also repairs the |
|
||||||
// display of older rows whose adapter canvas_id does not match the |
|
||||||
// annotation's target.source. |
|
||||||
const targetCanvasId = annotationTargetCanvasId(item.annotation); |
|
||||||
const canvasId = targetCanvasId && manifestCanvasIds.has(targetCanvasId) |
|
||||||
? targetCanvasId |
|
||||||
: item.canvasId; |
|
||||||
|
|
||||||
if (!byCanvas.has(canvasId)) byCanvas.set(canvasId, []); |
|
||||||
byCanvas.get(canvasId).push(item.annotation); |
|
||||||
}); |
|
||||||
|
|
||||||
const nextGroups = canvases |
|
||||||
.map((canvas) => ({ ...canvas, annotations: byCanvas.get(canvas.id) || [] })) |
|
||||||
.filter((canvas) => canvas.annotations.length > 0); |
|
||||||
|
|
||||||
if (!cancelled) setGroups(nextGroups); |
|
||||||
} |
|
||||||
catch (e) { |
|
||||||
if (!cancelled) setError(e?.message || String(e)); |
|
||||||
} |
|
||||||
finally { |
|
||||||
if (!cancelled) setLoading(false); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
load(); |
|
||||||
return () => { cancelled = true; }; |
|
||||||
}, [manifestId, endpoint]); |
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!pendingFocus || selectedCanvasId !== pendingFocus.canvas.id) return; |
|
||||||
|
|
||||||
// Mirador selection state drives the visual highlight. It is safe to set |
|
||||||
// this before the annotation page finishes loading; the overlay picks it |
|
||||||
// up as soon as the persisted annotation is received into the store. |
|
||||||
updateWindow(windowId, { highlightAllAnnotations: false }); |
|
||||||
selectAnnotation(windowId, pendingFocus.annotation.id); |
|
||||||
|
|
||||||
// The Redux canvas id changes before OpenSeadragon has necessarily |
|
||||||
// replaced the old tiled image. Wait until OSD is displaying the target |
|
||||||
// canvas, then reassert selection, redraw the annotation overlay, and zoom. |
|
||||||
focusAnnotationWhenViewerReady( |
|
||||||
windowId, |
|
||||||
pendingFocus.annotation, |
|
||||||
pendingFocus.canvas, |
|
||||||
selectAnnotation, |
|
||||||
updateWindow, |
|
||||||
); |
|
||||||
|
|
||||||
setPendingFocus(null); |
|
||||||
}, [pendingFocus, selectedCanvasId, selectAnnotation, updateWindow, windowId]); |
|
||||||
|
|
||||||
const focusAnnotation = (canvas, annotation) => { |
|
||||||
if (!annotation?.id) { |
|
||||||
setCanvas(windowId, canvas.id); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
setPendingFocus({ canvas, annotation }); |
|
||||||
|
|
||||||
if (selectedCanvasId === canvas.id) { |
|
||||||
updateWindow(windowId, { highlightAllAnnotations: false }); |
|
||||||
selectAnnotation(windowId, annotation.id); |
|
||||||
focusAnnotationWhenViewerReady( |
|
||||||
windowId, |
|
||||||
annotation, |
|
||||||
canvas, |
|
||||||
selectAnnotation, |
|
||||||
updateWindow, |
|
||||||
); |
|
||||||
setPendingFocus(null); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
setCanvas(windowId, canvas.id); |
|
||||||
}; |
|
||||||
|
|
||||||
const filteredGroups = useMemo(() => { |
|
||||||
const needle = filter.trim().toLowerCase(); |
|
||||||
if (!needle) return groups; |
|
||||||
return groups.map((group) => ({ |
|
||||||
...group, |
|
||||||
annotations: group.annotations.filter((annotation) => |
|
||||||
`${group.label} ${annotationText(annotation)}`.toLowerCase().includes(needle)), |
|
||||||
})).filter((group) => group.annotations.length > 0); |
|
||||||
}, [groups, filter]); |
|
||||||
|
|
||||||
const total = groups.reduce((sum, group) => sum + group.annotations.length, 0); |
|
||||||
|
|
||||||
return ( |
|
||||||
<CompanionWindow id={id} windowId={windowId} title={`All annotations (${total})`}> |
|
||||||
<Box sx={{ p: 1.5 }}> |
|
||||||
<TextField |
|
||||||
fullWidth |
|
||||||
size="small" |
|
||||||
label="Search annotations" |
|
||||||
value={filter} |
|
||||||
onChange={(event) => setFilter(event.target.value)} |
|
||||||
/> |
|
||||||
</Box> |
|
||||||
|
|
||||||
{loading && ( |
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> |
|
||||||
<CircularProgress size={28} /> |
|
||||||
</Box> |
|
||||||
)} |
|
||||||
|
|
||||||
{!loading && error && ( |
|
||||||
<Typography color="error" sx={{ px: 2, py: 1 }} variant="body2"> |
|
||||||
{error} |
|
||||||
</Typography> |
|
||||||
)} |
|
||||||
|
|
||||||
{!loading && !error && total === 0 && ( |
|
||||||
<Typography sx={{ px: 2, py: 1 }} variant="body2"> |
|
||||||
No annotations in this manifest. |
|
||||||
</Typography> |
|
||||||
)} |
|
||||||
|
|
||||||
{!loading && !error && filteredGroups.map((group) => ( |
|
||||||
<Box key={group.id}> |
|
||||||
<Typography |
|
||||||
component="h3" |
|
||||||
sx={{ px: 2, pt: 1.5, pb: 0.5, fontWeight: 600 }} |
|
||||||
variant="subtitle2" |
|
||||||
> |
|
||||||
{group.label} ({group.annotations.length}) |
|
||||||
</Typography> |
|
||||||
<List dense disablePadding> |
|
||||||
{group.annotations.map((annotation, index) => ( |
|
||||||
<ListItemButton |
|
||||||
key={annotation.id || `${group.id}-${index}`} |
|
||||||
selected={selectedCanvasId === group.id && selectedAnnotationId === annotation.id} |
|
||||||
onClick={() => focusAnnotation(group, annotation)} |
|
||||||
sx={{ pl: 3, alignItems: 'flex-start' }} |
|
||||||
> |
|
||||||
<ListItemText |
|
||||||
primary={annotationText(annotation)} |
|
||||||
primaryTypographyProps={{ variant: 'body2' }} |
|
||||||
/> |
|
||||||
</ListItemButton> |
|
||||||
))} |
|
||||||
</List> |
|
||||||
<Divider /> |
|
||||||
</Box> |
|
||||||
))} |
|
||||||
</CompanionWindow> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
const allAnnotationsPanelPlugin = { |
|
||||||
name: 'AllAnnotationsPanel', |
|
||||||
companionWindowKey: PANEL_KEY, |
|
||||||
component: AllAnnotationsPanel, |
|
||||||
mapStateToProps: (state, { windowId }) => ({ |
|
||||||
manifestId: state?.windows?.[windowId]?.manifestId, |
|
||||||
selectedCanvasId: state?.windows?.[windowId]?.canvasId, |
|
||||||
selectedAnnotationId: state?.windows?.[windowId]?.selectedAnnotationId, |
|
||||||
endpoint: state?.config?.annotation?.allEndpoint, |
|
||||||
}), |
|
||||||
mapDispatchToProps: (dispatch) => ({ |
|
||||||
setCanvas: (windowId, canvasId) => dispatch(setCanvasAction(windowId, canvasId)), |
|
||||||
selectAnnotation: (windowId, annotationId) => |
|
||||||
dispatch(selectAnnotationAction(windowId, annotationId)), |
|
||||||
updateWindow: (windowId, payload) => |
|
||||||
dispatch(updateWindowAction(windowId, payload)), |
|
||||||
}), |
|
||||||
}; |
|
||||||
|
|
||||||
const AllAnnotationsButton = () => <AllAnnotationsIcon fontSize="small" />; |
|
||||||
AllAnnotationsButton.value = PANEL_KEY; |
|
||||||
|
|
||||||
const allAnnotationsButtonPlugin = { |
|
||||||
target: 'WindowSideBarButtons', |
|
||||||
mode: 'add', |
|
||||||
name: 'AllAnnotationsButton', |
|
||||||
value: PANEL_KEY, |
|
||||||
component: AllAnnotationsButton, |
|
||||||
}; |
|
||||||
|
|
||||||
export { allAnnotationsPanelPlugin, allAnnotationsButtonPlugin }; |
|
||||||
File diff suppressed because one or more lines are too long
@ -1,7 +1,10 @@ |
|||||||
annotated_mirador: |
annotated_mirador: |
||||||
version: 0.2.5 |
version: 0.1.0 |
||||||
|
|
||||||
js: |
js: |
||||||
dist/main.js: { minified: true, preprocess: false } |
dist/main.js: |
||||||
|
minified: true |
||||||
|
|
||||||
css: |
css: |
||||||
theme: |
theme: |
||||||
dist/main.css: { preprocess: false } |
dist/main.css: {} |
||||||
|
|||||||
Loading…
Reference in new issue