Skip to content
Snippets Groups Projects
Commit ed51af76 authored by David Beniamine's avatar David Beniamine
Browse files

Merge branch '5-images-in-annotations' into 'tetras-main'

Create image annotation creation dialog

Closes #5

See merge request iiif/mirador-annotations!5
parents e79e44b3 452746bb
Branches
No related tags found
1 merge request!5Create image annotation creation dialog
Pipeline #1342 passed
...@@ -4,6 +4,7 @@ import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; ...@@ -4,6 +4,7 @@ import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import AnnotationCreation from '../src/AnnotationCreation'; import AnnotationCreation from '../src/AnnotationCreation';
import AnnotationDrawing from '../src/AnnotationDrawing'; import AnnotationDrawing from '../src/AnnotationDrawing';
import TextEditor from '../src/TextEditor'; import TextEditor from '../src/TextEditor';
import ImageFormField from '../src/ImageFormField';
/** */ /** */
function createWrapper(props) { function createWrapper(props) {
...@@ -36,6 +37,10 @@ describe('AnnotationCreation', () => { ...@@ -36,6 +37,10 @@ describe('AnnotationCreation', () => {
wrapper = createWrapper(); wrapper = createWrapper();
expect(wrapper.dive().find(TextEditor).length).toBe(1); expect(wrapper.dive().find(TextEditor).length).toBe(1);
}); });
it('adds the ImageFormField component', () => {
wrapper = createWrapper();
expect(wrapper.dive().find(ImageFormField).length).toBe(1);
});
it('can handle annotations without target selector', () => { it('can handle annotations without target selector', () => {
wrapper = createWrapper({ wrapper = createWrapper({
annotation: { annotation: {
......
...@@ -3,7 +3,9 @@ import WebAnnotation from '../src/WebAnnotation'; ...@@ -3,7 +3,9 @@ import WebAnnotation from '../src/WebAnnotation';
/** */ /** */
function createSubject(args = {}) { function createSubject(args = {}) {
return new WebAnnotation({ return new WebAnnotation({
body: 'body', body: {
value: 'body',
},
canvasId: 'canvasId', canvasId: 'canvasId',
fragsel: { t: '5,10', xywh: 'xywh' }, fragsel: { t: '5,10', xywh: 'xywh' },
id: 'id', id: 'id',
...@@ -17,11 +19,16 @@ describe('WebAnnotation', () => { ...@@ -17,11 +19,16 @@ describe('WebAnnotation', () => {
let subject = createSubject(); let subject = createSubject();
describe('constructor', () => { describe('constructor', () => {
it('sets instance accessors', () => { it('sets instance accessors', () => {
['body', 'canvasId', 'id', 'svg'].forEach((prop) => { ['canvasId', 'id', 'svg'].forEach((prop) => {
expect(subject[prop]).toBe(prop); expect(subject[prop]).toBe(prop);
}); });
expect(subject.fragsel).toStrictEqual({ t: '5,10', xywh: 'xywh' }); expect(subject.fragsel).toStrictEqual({ t: '5,10', xywh: 'xywh' });
}); });
it('sets instance accessors for body', () => {
['body'].forEach((prop) => {
expect(subject[prop].value).toBe(prop);
});
});
}); });
describe('target', () => { describe('target', () => {
it('with svg and xywh', () => { it('with svg and xywh', () => {
...@@ -109,20 +116,40 @@ describe('WebAnnotation', () => { ...@@ -109,20 +116,40 @@ describe('WebAnnotation', () => {
]); ]);
}); });
it('with text only', () => { it('with text only', () => {
subject = createSubject({ tags: null }); subject = createSubject({ image: null, tags: null });
expect(subject.createBody()).toEqual({ expect(subject.createBody()).toEqual({
type: 'TextualBody', type: 'TextualBody',
value: 'body', value: 'body',
}); });
}); });
it('with tags only', () => { it('with tags only', () => {
subject = createSubject({ body: null }); subject = createSubject({ body: null, image: null });
expect(subject.createBody()).toEqual({ expect(subject.createBody()).toEqual({
purpose: 'tagging', purpose: 'tagging',
type: 'TextualBody', type: 'TextualBody',
value: 'tags', value: 'tags',
}); });
}); });
it('with image and text', () => {
subject = createSubject({ body: { value: 'hello' }, image: { id: 'http://example.photo/pic.jpg' }, tags: null });
expect(subject.createBody()).toEqual([
{
type: 'TextualBody',
value: 'hello',
},
{
id: 'http://example.photo/pic.jpg',
type: 'Image',
},
]);
});
it('with image only', () => {
subject = createSubject({ body: null, image: { id: 'http://example.photo/pic.jpg' }, tags: null });
expect(subject.createBody()).toEqual({
id: 'http://example.photo/pic.jpg',
type: 'Image',
});
});
}); });
describe('toJson', () => { describe('toJson', () => {
it('generates a WebAnnotation', () => { it('generates a WebAnnotation', () => {
......
...@@ -15514,7 +15514,7 @@ ...@@ -15514,7 +15514,7 @@
}, },
"node_modules/mirador": { "node_modules/mirador": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#8d142157eeb008edd0761859b6ad8abfa564c2a6", "resolved": "git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#047b206353616adc135bcd3b018da9857c4222d6",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
...@@ -34987,7 +34987,7 @@ ...@@ -34987,7 +34987,7 @@
} }
}, },
"mirador": { "mirador": {
"version": "git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#8d142157eeb008edd0761859b6ad8abfa564c2a6", "version": "git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#047b206353616adc135bcd3b018da9857c4222d6",
"dev": true, "dev": true,
"from": "mirador@git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#annotation-on-video", "from": "mirador@git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#annotation-on-video",
"requires": { "requires": {
......
...@@ -19,6 +19,7 @@ import StrokeColorIcon from '@material-ui/icons/BorderColor'; ...@@ -19,6 +19,7 @@ import StrokeColorIcon from '@material-ui/icons/BorderColor';
import LineWeightIcon from '@material-ui/icons/LineWeight'; import LineWeightIcon from '@material-ui/icons/LineWeight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import FormatShapesIcon from '@material-ui/icons/FormatShapes'; import FormatShapesIcon from '@material-ui/icons/FormatShapes';
import TextField from '@material-ui/core/TextField';
import { SketchPicker } from 'react-color'; import { SketchPicker } from 'react-color';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
...@@ -30,6 +31,7 @@ import TextEditor from './TextEditor'; ...@@ -30,6 +31,7 @@ import TextEditor from './TextEditor';
import WebAnnotation from './WebAnnotation'; import WebAnnotation from './WebAnnotation';
import CursorIcon from './icons/Cursor'; import CursorIcon from './icons/Cursor';
import HMSInput from './HMSInput'; import HMSInput from './HMSInput';
import ImageFormField from './ImageFormField';
import { secondsToHMS } from './utils'; import { secondsToHMS } from './utils';
/** Extract time information from annotation target */ /** Extract time information from annotation target */
...@@ -60,20 +62,27 @@ class AnnotationCreation extends Component { ...@@ -60,20 +62,27 @@ class AnnotationCreation extends Component {
super(props); super(props);
const annoState = {}; const annoState = {};
if (props.annotation) { if (props.annotation) {
// //
// annotation body // annotation body
if (Array.isArray(props.annotation.body)) { if (Array.isArray(props.annotation.body)) {
annoState.tags = []; annoState.tags = [];
props.annotation.body.forEach((body) => { props.annotation.body.forEach((body) => {
if (body.purpose === 'tagging') { if (body.purpose === 'tagging' && body.type === 'TextualBody') {
annoState.tags.push(body.value); annoState.tags.push(body.value);
} else { } else if (body.type === 'TextualBody') {
annoState.annoBody = body.value; annoState.textBody = body.value;
} else if (body.type === 'Image') {
// annoState.textBody = body.value; // why text body here ???
annoState.image = body;
} }
}); });
} else { } else if (props.annotation.body.type === 'TextualBody') {
annoState.annoBody = props.annotation.body.value; annoState.textBody = props.annotation.body.value;
} else if (props.annotation.body.type === 'Image') {
// annoState.textBody = props.annotation.body.value; // why text body here ???
annoState.image = props.annotation.body;
} }
// //
// drawing position // drawing position
...@@ -101,6 +110,7 @@ class AnnotationCreation extends Component { ...@@ -101,6 +110,7 @@ class AnnotationCreation extends Component {
const toolState = { const toolState = {
activeTool: 'cursor', activeTool: 'cursor',
closedMode: 'closed', closedMode: 'closed',
colorPopoverOpen: false,
currentColorType: false, currentColorType: false,
fillColor: null, fillColor: null,
strokeColor: '#00BFFF', strokeColor: '#00BFFF',
...@@ -115,19 +125,24 @@ class AnnotationCreation extends Component { ...@@ -115,19 +125,24 @@ class AnnotationCreation extends Component {
this.state = { this.state = {
...toolState, ...toolState,
...timeState, ...timeState,
annoBody: '', activeTool: 'cursor',
colorPopoverOpen: false, closedMode: 'closed',
currentColorType: false,
fillColor: null,
image: { id: null },
lineWeightPopoverOpen: false, lineWeightPopoverOpen: false,
popoverAnchorEl: null, popoverAnchorEl: null,
popoverLineWeightAnchorEl: null, popoverLineWeightAnchorEl: null,
svg: null, svg: null,
textBody: '',
textEditorStateBustingKey: 0, textEditorStateBustingKey: 0,
xywh: null, xywh: null,
...annoState, ...annoState,
}; };
this.submitForm = this.submitForm.bind(this); this.submitForm = this.submitForm.bind(this);
this.updateBody = this.updateBody.bind(this); // this.updateBody = this.updateBody.bind(this);
this.updateTextBody = this.updateTextBody.bind(this);
this.updateTstart = this.updateTstart.bind(this); this.updateTstart = this.updateTstart.bind(this);
this.updateTend = this.updateTend.bind(this); this.updateTend = this.updateTend.bind(this);
this.setTstartNow = this.setTstartNow.bind(this); this.setTstartNow = this.setTstartNow.bind(this);
...@@ -143,6 +158,13 @@ class AnnotationCreation extends Component { ...@@ -143,6 +158,13 @@ class AnnotationCreation extends Component {
this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this); this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this);
this.closeChooseColor = this.closeChooseColor.bind(this); this.closeChooseColor = this.closeChooseColor.bind(this);
this.updateStrokeColor = this.updateStrokeColor.bind(this); this.updateStrokeColor = this.updateStrokeColor.bind(this);
this.handleImgChange = this.handleImgChange.bind(this);
}
/** */
handleImgChange(newUrl, imgRef) {
const { image } = this.state;
this.setState({ image: { ...image, id: newUrl } });
} }
/** */ /** */
...@@ -174,12 +196,6 @@ class AnnotationCreation extends Component { ...@@ -174,12 +196,6 @@ class AnnotationCreation extends Component {
this.setState({ tend: Math.floor(this.props.currentTime) }); this.setState({ tend: Math.floor(this.props.currentTime) });
} }
/** update annotation start time */
updateTstart(value) { this.setState({ tstart: value }); }
/** update annotation end time */
updateTend(value) { this.setState({ tend: value }); }
/** seekTo/goto annotation start time */ /** seekTo/goto annotation start time */
seekToTstart() { seekToTstart() {
const { paused, setCurrentTime, setSeekTo } = this.props; const { paused, setCurrentTime, setSeekTo } = this.props;
...@@ -202,6 +218,12 @@ class AnnotationCreation extends Component { ...@@ -202,6 +218,12 @@ class AnnotationCreation extends Component {
} }
} }
/** update annotation start time */
updateTstart(value) { this.setState({ tstart: value }); }
/** update annotation end time */
updateTend(value) { this.setState({ tend: value }); }
/** */ /** */
openChooseColor(e) { openChooseColor(e) {
this.setState({ this.setState({
...@@ -243,20 +265,25 @@ class AnnotationCreation extends Component { ...@@ -243,20 +265,25 @@ class AnnotationCreation extends Component {
annotation, canvases, receiveAnnotation, config, annotation, canvases, receiveAnnotation, config,
} = this.props; } = this.props;
const { const {
annoBody, tags, xywh, svg, tstart, tend, textEditorStateBustingKey, textBody, image, tags, xywh, svg, tstart, tend, textEditorStateBustingKey,
} = this.state; } = this.state;
const t = (tstart && tend) ? `${tstart},${tend}` : null; const t = (tstart && tend) ? `${tstart},${tend}` : null;
const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody };
canvases.forEach((canvas) => { canvases.forEach((canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id); const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({ const anno = new WebAnnotation({
body: (!annoBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody, body,
canvasId: canvas.id, canvasId: canvas.id,
fragsel: { t, xywh }, fragsel: { t, xywh },
id: (annotation && annotation.id) || `${uuid()}`, id: (annotation && annotation.id) || `${uuid()}`,
image,
manifestId: canvas.options.resource.id, manifestId: canvas.options.resource.id,
svg, svg,
tags, tags,
}).toJson(); }).toJson();
if (annotation) { if (annotation) {
storageAdapter.update(anno).then((annoPage) => { storageAdapter.update(anno).then((annoPage) => {
receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
...@@ -269,9 +296,10 @@ class AnnotationCreation extends Component { ...@@ -269,9 +296,10 @@ class AnnotationCreation extends Component {
}); });
this.setState({ this.setState({
annoBody: '', image: { id: null },
svg: null, svg: null,
tend: null, tend: null,
textBody: '',
textEditorStateBustingKey: textEditorStateBustingKey + 1, textEditorStateBustingKey: textEditorStateBustingKey + 1,
tstart: null, tstart: null,
xywh: null, xywh: null,
...@@ -293,8 +321,8 @@ class AnnotationCreation extends Component { ...@@ -293,8 +321,8 @@ class AnnotationCreation extends Component {
} }
/** */ /** */
updateBody(annoBody) { updateTextBody(textBody) {
this.setState({ annoBody }); this.setState({ textBody });
} }
/** */ /** */
...@@ -311,9 +339,10 @@ class AnnotationCreation extends Component { ...@@ -311,9 +339,10 @@ class AnnotationCreation extends Component {
} = this.props; } = this.props;
const { const {
activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, strokeColor, activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl,
popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, annoBody, svg, strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode,
tstart, tend, textEditorStateBustingKey, textBody, svg, tstart, tend,
textEditorStateBustingKey, image,
} = this.state; } = this.state;
const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined'; const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined';
...@@ -326,6 +355,7 @@ class AnnotationCreation extends Component { ...@@ -326,6 +355,7 @@ class AnnotationCreation extends Component {
> >
<AnnotationDrawing <AnnotationDrawing
activeTool={activeTool} activeTool={activeTool}
annotation={annotation}
fillColor={fillColor} fillColor={fillColor}
strokeColor={strokeColor} strokeColor={strokeColor}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
...@@ -440,7 +470,6 @@ class AnnotationCreation extends Component { ...@@ -440,7 +470,6 @@ class AnnotationCreation extends Component {
) )
: null : null
} }
</Grid> </Grid>
</Grid> </Grid>
<Grid container> <Grid container>
...@@ -464,7 +493,7 @@ class AnnotationCreation extends Component { ...@@ -464,7 +493,7 @@ class AnnotationCreation extends Component {
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="overline"> <Typography variant="overline">
<ToggleButton value="true" title="Go to start time" size="small" onClick={this.seekToTend} className={classes.timecontrolsbutton}> <ToggleButton value="true" title="Go to end time" size="small" onClick={this.seekToTend} className={classes.timecontrolsbutton}>
<LastPage /> <LastPage />
</ToggleButton> </ToggleButton>
End End
...@@ -481,14 +510,22 @@ class AnnotationCreation extends Component { ...@@ -481,14 +510,22 @@ class AnnotationCreation extends Component {
)} )}
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="overline"> <Typography variant="overline">
Content Image Content
</Typography>
</Grid>
<Grid item xs={12} style={{ marginBottom: 10 }}>
<ImageFormField value={image} onChange={this.handleImgChange} />
</Grid>
<Grid item xs={12}>
<Typography variant="overline">
Text Content
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TextEditor <TextEditor
key={textEditorStateBustingKey} key={textEditorStateBustingKey}
annoHtml={annoBody} annoHtml={textBody}
updateAnnotationBody={this.updateBody} updateAnnotationBody={this.updateTextBody}
/> />
</Grid> </Grid>
</Grid> </Grid>
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { TextField } from '@material-ui/core';
/** URL input with an <img> preview */
class ImageFormField extends Component {
/** */
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
/** Render input field and a preview if the input is valid */
render() {
const { value: image, classes, onChange } = this.props;
const imgIsValid = this.inputRef.current
? (image.id && this.inputRef.current.checkValidity()) : image.id;
const imgUrl = image.id === null ? '' : image.id;
return (
<div className={classes.root}>
<TextField
value={imgUrl}
onChange={(ev) => onChange(ev.target.value)}
error={imgUrl !== '' && !imgIsValid}
margin="dense"
label="Image URL"
type="url"
fullWidth
inputRef={this.inputRef}
/>
{ imgIsValid
&& <img src={image.id} width="100%" height="auto" alt="loading failed" /> }
</div>
);
}
}
/** custom css */
const styles = (theme) => ({
root: {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
},
});
ImageFormField.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
classes: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
};
ImageFormField.defaultProps = {
};
export default withStyles(styles)(ImageFormField);
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
export default class WebAnnotation { export default class WebAnnotation {
/** */ /** */
constructor({ constructor({
canvasId, id, fragsel, body, tags, svg, manifestId, canvasId, id, fragsel, image, body, tags, svg, manifestId,
}) { }) {
this.id = id; this.id = id;
this.canvasId = canvasId; this.canvasId = canvasId;
...@@ -10,6 +10,7 @@ export default class WebAnnotation { ...@@ -10,6 +10,7 @@ export default class WebAnnotation {
this.body = body; this.body = body;
this.tags = tags; this.tags = tags;
this.svg = svg; this.svg = svg;
this.image = image;
this.manifestId = manifestId; this.manifestId = manifestId;
} }
...@@ -27,12 +28,23 @@ export default class WebAnnotation { ...@@ -27,12 +28,23 @@ export default class WebAnnotation {
/** */ /** */
createBody() { createBody() {
let bodies = []; let bodies = [];
if (this.body) {
bodies.push({ if (this.body && this.body.value !== '') {
const textBody = {
type: 'TextualBody', type: 'TextualBody',
value: this.body, value: this.body.value,
}); };
bodies.push(textBody);
} }
if (this.image) {
const imgBody = {
id: this.image.id,
type: 'Image',
};
bodies.push(imgBody);
}
if (this.tags) { if (this.tags) {
bodies = bodies.concat(this.tags.map((tag) => ({ bodies = bodies.concat(this.tags.map((tag) => ({
purpose: 'tagging', purpose: 'tagging',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment