Skip to content
Snippets Groups Projects
Commit 779802fa authored by Chris Beer's avatar Chris Beer
Browse files

Add some indication about annotations in the gallery view

parent 5cc17197
No related branches found
No related tags found
No related merge requests found
......@@ -2,6 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import Chip from '@material-ui/core/Chip';
import IntersectionObserver from '@researchgate/react-intersection-observer';
import manifestJson from '../../fixtures/version-2/019.json';
import { GalleryViewThumbnail } from '../../../src/components/GalleryViewThumbnail';
import IIIFThumbnail from '../../../src/containers/IIIFThumbnail';
......@@ -61,6 +62,57 @@ describe('GalleryView', () => {
expect(focusOnCanvas).toHaveBeenCalled();
});
describe('on-demand annotation fetching', () => {
it('fetches IIIF v2 annotations', () => {
const requestAnnotation = jest.fn();
const canvas = {
__jsonld: {
otherContent: [
'alreadyFetched',
'some-uri',
],
},
getHeight: () => 50,
getWidth: () => 50,
};
wrapper = createWrapper({ annotations: { alreadyFetched: true }, canvas, requestAnnotation });
wrapper.find(IntersectionObserver).simulate('change', { isIntersecting: true });
expect(requestAnnotation).toHaveBeenCalledWith('some-uri');
expect(requestAnnotation).not.toHaveBeenCalledWith('alreadyFetched');
});
it('requests IIIF v3-style annotations for each visible canvas', () => {
const requestAnnotation = jest.fn();
const canvas = {
__jsonld: {
annotations: { id: 'annoId', type: 'AnnotationPage' },
},
getHeight: () => 50,
getWidth: () => 50,
};
wrapper = createWrapper({ annotations: {}, canvas, requestAnnotation });
wrapper.find(IntersectionObserver).simulate('change', { isIntersecting: true });
expect(requestAnnotation).toHaveBeenCalledWith('annoId');
});
it('handles embedded IIIF v3-style annotations on each visible canvas', () => {
const receiveAnnotation = jest.fn();
const annotations = { id: 'annoId', items: [], type: 'AnnotationPage' };
const canvas = {
__jsonld: {
annotations,
},
getHeight: () => 50,
getWidth: () => 50,
};
wrapper = createWrapper({ annotations: {}, canvas, receiveAnnotation });
wrapper.find(IntersectionObserver).simulate('change', { isIntersecting: true });
expect(receiveAnnotation).toHaveBeenCalledWith(annotations);
});
});
describe('annotation count chip', () => {
it('hides the chip if there are no annotations', () => {
wrapper = createWrapper({ annotationsCount: 0 });
......@@ -71,7 +123,19 @@ describe('GalleryView', () => {
wrapper = createWrapper({ annotationsCount: 50 });
expect(wrapper.find(Chip).length).toEqual(1);
expect(wrapper.find(Chip).prop('label')).toEqual(50);
expect(wrapper.find(Chip).prop('className')).toEqual('');
});
});
describe('search annotation count chip', () => {
it('hides the chip if there are no annotations', () => {
wrapper = createWrapper({ searchAnnotationsCount: 0 });
expect(wrapper.find(Chip).length).toEqual(0);
});
it('shows the number of search annotations on a canvas', () => {
wrapper = createWrapper({ searchAnnotationsCount: 50 });
expect(wrapper.find(Chip).length).toEqual(1);
expect(wrapper.find(Chip).prop('label')).toEqual(50);
});
});
});
......@@ -2,8 +2,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Avatar from '@material-ui/core/Avatar';
import Chip from '@material-ui/core/Chip';
import AnnotationIcon from '@material-ui/icons/CommentSharp';
import SearchIcon from '@material-ui/icons/SearchSharp';
import classNames from 'classnames';
import 'intersection-observer'; // polyfill needed for Safari
import IntersectionObserver from '@researchgate/react-intersection-observer';
import MiradorCanvas from '../lib/MiradorCanvas';
import IIIFThumbnail from '../containers/IIIFThumbnail';
......@@ -18,6 +21,7 @@ export class GalleryViewThumbnail extends Component {
this.handleSelect = this.handleSelect.bind(this);
this.handleKey = this.handleKey.bind(this);
this.handleIntersection = this.handleIntersection.bind(this);
}
/** @private */
......@@ -63,24 +67,54 @@ export class GalleryViewThumbnail extends Component {
}
}
/** */
handleIntersection({ isIntersecting }) {
const {
annotations,
canvas,
requestAnnotation, receiveAnnotation,
} = this.props;
if (!isIntersecting) return;
const { annotationListUris = [], canvasAnnotationPages = [] } = new MiradorCanvas(canvas);
annotationListUris
.filter(uri => !(annotations[uri]))
.forEach(uri => requestAnnotation(uri));
// IIIF v3
canvasAnnotationPages
.filter(annotation => !(annotations[annotation.id]))
.forEach((annotation) => {
// If there are no items, try to retrieve the referenced resource.
// otherwise the resource should be embedded and just add to the store.
if (!annotation.items) {
requestAnnotation(annotation.id);
} else {
receiveAnnotation(annotation);
}
});
}
/**
* Renders things
*/
render() {
const {
annotationsCount, canvas, classes, config, selected,
annotationsCount, searchAnnotationsCount,
canvas, classes, config, selected,
} = this.props;
const miradorCanvas = new MiradorCanvas(canvas);
return (
<IntersectionObserver onChange={this.handleIntersection}>
<div
key={canvas.index}
className={
classNames(
classes.galleryViewItem,
selected ? classes.selected : '',
annotationsCount > 0 ? classes.hasAnnotations : '',
searchAnnotationsCount > 0 ? classes.hasAnnotations : '',
)
}
onClick={this.handleSelect}
......@@ -103,25 +137,39 @@ export class GalleryViewThumbnail extends Component {
<Chip
avatar={(
<Avatar className={classes.avatar} classes={{ circle: classes.avatarIcon }}>
<SearchIcon fontSize="small" />
<AnnotationIcon className={classes.annotationIcon} />
</Avatar>
)}
label={annotationsCount}
className={
classNames(
classes.chip,
classes.annotationsChip,
)
}
size="small"
/>
)}
{ searchAnnotationsCount > 0 && (
<Chip
avatar={(
<Avatar className={classes.avatar} classes={{ circle: classes.avatarIcon }}>
<SearchIcon fontSize="small" />
</Avatar>
)}
label={searchAnnotationsCount}
className={classNames(classes.searchChip)}
size="small"
/>
)}
</IIIFThumbnail>
</div>
</IntersectionObserver>
);
}
}
GalleryViewThumbnail.propTypes = {
annotations: PropTypes.objectOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
annotationsCount: PropTypes.number,
canvas: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
classes: PropTypes.objectOf(PropTypes.string).isRequired,
......@@ -130,15 +178,22 @@ GalleryViewThumbnail.propTypes = {
width: PropTypes.number,
}),
focusOnCanvas: PropTypes.func.isRequired,
receiveAnnotation: PropTypes.func,
requestAnnotation: PropTypes.func,
searchAnnotationsCount: PropTypes.number,
selected: PropTypes.bool,
setCanvas: PropTypes.func.isRequired,
};
GalleryViewThumbnail.defaultProps = {
annotations: {},
annotationsCount: 0,
config: {
height: 100,
width: null,
},
receiveAnnotation: () => {},
requestAnnotation: () => {},
searchAnnotationsCount: 0,
selected: false,
};
......@@ -8,22 +8,29 @@ import {
getSearchAnnotationsForWindow,
getCurrentCanvas,
getConfig,
getAnnotations,
getPresentAnnotationsOnSelectedCanvases,
getCompanionWindowsForContent,
} from '../state/selectors';
/**
* Styles to be passed to the withStyles HOC
*/
const styles = theme => ({
avatar: {
backgroundColor: 'transparent',
annotationIcon: {
height: '1rem',
width: '1rem',
},
chip: {
annotationsChip: {
...theme.typography.caption,
left: '50%',
position: 'absolute',
top: 80,
top: 10,
transform: 'translate(-50%, 0)',
},
avatar: {
backgroundColor: 'transparent',
},
galleryViewItem: {
'&$hasAnnotations': {
border: `2px solid ${theme.palette.action.selected}`,
......@@ -49,6 +56,16 @@ const styles = theme => ({
width: 'min-content',
},
hasAnnotations: {},
searchChip: {
...theme.typography.caption,
'&$selected $avatar': {
backgroundColor: theme.palette.highlights.primary,
},
left: '50%',
position: 'absolute',
top: 80,
transform: 'translate(-50%, 0)',
},
selected: {},
});
......@@ -63,9 +80,20 @@ const mapStateToProps = (state, { canvas, windowId }) => {
const canvasAnnotations = flatten(searchAnnotations.map(a => a.resources))
.filter(a => a.targetId === canvas.id);
const hasOpenAnnotationsWindow = getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0;
return {
annotationsCount: canvasAnnotations.length,
annotations: hasOpenAnnotationsWindow
? getAnnotations(state, { windowId })[canvas.id] || {}
: {},
annotationsCount: hasOpenAnnotationsWindow
? getPresentAnnotationsOnSelectedCanvases(
state,
{ canvasId: canvas.id },
).reduce((v, a) => v + a.resources.filter(r => r.targetId === canvas.id).length, 0)
: 0,
config: getConfig(state).galleryView,
searchAnnotationsCount: canvasAnnotations.length,
selected: currentCanvas && currentCanvas.id === canvas.id,
};
};
......@@ -75,8 +103,12 @@ const mapStateToProps = (state, { canvas, windowId }) => {
* @memberof WindowViewer
* @private
*/
const mapDispatchToProps = (dispatch, { id, windowId }) => ({
const mapDispatchToProps = (dispatch, { canvas, id, windowId }) => ({
focusOnCanvas: () => dispatch(actions.setWindowViewType(windowId, 'single')),
receiveAnnotation: (annotation) => (
dispatch(actions.receiveAnnotation(canvas.id, annotation.id, annotation))
),
requestAnnotation: (...args) => dispatch(actions.requestAnnotation(canvas.id, ...args)),
setCanvas: (...args) => dispatch(actions.setCanvas(windowId, ...args)),
});
......
......@@ -44,7 +44,10 @@ const getPresentAnnotationsCanvas = createSelector(
const getAnnotationsOnSelectedCanvases = createSelector(
[
getVisibleCanvasIds,
(state, { canvasId, ...otherProps }) => (canvasId
? [canvasId]
: getVisibleCanvasIds(state, otherProps)
),
getAnnotations,
],
(canvasIds, annotations) => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment