Skip to content
Snippets Groups Projects
Commit 452746bb authored by Loïs Poujade's avatar Loïs Poujade Committed by David Beniamine
Browse files

Create image annotation creation dialog

parent e79e44b3
No related branches found
No related tags found
1 merge request!5Create image annotation creation dialog
......@@ -4,6 +4,7 @@ import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import AnnotationCreation from '../src/AnnotationCreation';
import AnnotationDrawing from '../src/AnnotationDrawing';
import TextEditor from '../src/TextEditor';
import ImageFormField from '../src/ImageFormField';
/** */
function createWrapper(props) {
......@@ -36,6 +37,10 @@ describe('AnnotationCreation', () => {
wrapper = createWrapper();
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', () => {
wrapper = createWrapper({
annotation: {
......
......@@ -3,7 +3,9 @@ import WebAnnotation from '../src/WebAnnotation';
/** */
function createSubject(args = {}) {
return new WebAnnotation({
body: 'body',
body: {
value: 'body',
},
canvasId: 'canvasId',
fragsel: { t: '5,10', xywh: 'xywh' },
id: 'id',
......@@ -17,11 +19,16 @@ describe('WebAnnotation', () => {
let subject = createSubject();
describe('constructor', () => {
it('sets instance accessors', () => {
['body', 'canvasId', 'id', 'svg'].forEach((prop) => {
['canvasId', 'id', 'svg'].forEach((prop) => {
expect(subject[prop]).toBe(prop);
});
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', () => {
it('with svg and xywh', () => {
......@@ -109,20 +116,40 @@ describe('WebAnnotation', () => {
]);
});
it('with text only', () => {
subject = createSubject({ tags: null });
subject = createSubject({ image: null, tags: null });
expect(subject.createBody()).toEqual({
type: 'TextualBody',
value: 'body',
});
});
it('with tags only', () => {
subject = createSubject({ body: null });
subject = createSubject({ body: null, image: null });
expect(subject.createBody()).toEqual({
purpose: 'tagging',
type: 'TextualBody',
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', () => {
it('generates a WebAnnotation', () => {
......
......@@ -15514,7 +15514,7 @@
},
"node_modules/mirador": {
"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,
"license": "Apache-2.0",
"dependencies": {
......@@ -34987,7 +34987,7 @@
}
},
"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,
"from": "mirador@git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#annotation-on-video",
"requires": {
......
......@@ -19,6 +19,7 @@ import StrokeColorIcon from '@material-ui/icons/BorderColor';
import LineWeightIcon from '@material-ui/icons/LineWeight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import FormatShapesIcon from '@material-ui/icons/FormatShapes';
import TextField from '@material-ui/core/TextField';
import { SketchPicker } from 'react-color';
import { v4 as uuid } from 'uuid';
import { withStyles } from '@material-ui/core/styles';
......@@ -30,6 +31,7 @@ import TextEditor from './TextEditor';
import WebAnnotation from './WebAnnotation';
import CursorIcon from './icons/Cursor';
import HMSInput from './HMSInput';
import ImageFormField from './ImageFormField';
import { secondsToHMS } from './utils';
/** Extract time information from annotation target */
......@@ -60,20 +62,27 @@ class AnnotationCreation extends Component {
super(props);
const annoState = {};
if (props.annotation) {
//
// annotation body
if (Array.isArray(props.annotation.body)) {
annoState.tags = [];
props.annotation.body.forEach((body) => {
if (body.purpose === 'tagging') {
if (body.purpose === 'tagging' && body.type === 'TextualBody') {
annoState.tags.push(body.value);
} else {
annoState.annoBody = body.value;
} else if (body.type === 'TextualBody') {
annoState.textBody = body.value;
} else if (body.type === 'Image') {
// annoState.textBody = body.value; // why text body here ???
annoState.image = body;
}
});
} else {
annoState.annoBody = props.annotation.body.value;
} else if (props.annotation.body.type === 'TextualBody') {
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
......@@ -101,6 +110,7 @@ class AnnotationCreation extends Component {
const toolState = {
activeTool: 'cursor',
closedMode: 'closed',
colorPopoverOpen: false,
currentColorType: false,
fillColor: null,
strokeColor: '#00BFFF',
......@@ -115,19 +125,24 @@ class AnnotationCreation extends Component {
this.state = {
...toolState,
...timeState,
annoBody: '',
colorPopoverOpen: false,
activeTool: 'cursor',
closedMode: 'closed',
currentColorType: false,
fillColor: null,
image: { id: null },
lineWeightPopoverOpen: false,
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
svg: null,
textBody: '',
textEditorStateBustingKey: 0,
xywh: null,
...annoState,
};
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.updateTend = this.updateTend.bind(this);
this.setTstartNow = this.setTstartNow.bind(this);
......@@ -143,6 +158,13 @@ class AnnotationCreation extends Component {
this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this);
this.closeChooseColor = this.closeChooseColor.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 {
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 */
seekToTstart() {
const { paused, setCurrentTime, setSeekTo } = this.props;
......@@ -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) {
this.setState({
......@@ -243,20 +265,25 @@ class AnnotationCreation extends Component {
annotation, canvases, receiveAnnotation, config,
} = this.props;
const {
annoBody, tags, xywh, svg, tstart, tend, textEditorStateBustingKey,
textBody, image, tags, xywh, svg, tstart, tend, textEditorStateBustingKey,
} = this.state;
const t = (tstart && tend) ? `${tstart},${tend}` : null;
const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody };
canvases.forEach((canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({
body: (!annoBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody,
body,
canvasId: canvas.id,
fragsel: { t, xywh },
id: (annotation && annotation.id) || `${uuid()}`,
image,
manifestId: canvas.options.resource.id,
svg,
tags,
}).toJson();
if (annotation) {
storageAdapter.update(anno).then((annoPage) => {
receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
......@@ -269,9 +296,10 @@ class AnnotationCreation extends Component {
});
this.setState({
annoBody: '',
image: { id: null },
svg: null,
tend: null,
textBody: '',
textEditorStateBustingKey: textEditorStateBustingKey + 1,
tstart: null,
xywh: null,
......@@ -293,8 +321,8 @@ class AnnotationCreation extends Component {
}
/** */
updateBody(annoBody) {
this.setState({ annoBody });
updateTextBody(textBody) {
this.setState({ textBody });
}
/** */
......@@ -311,9 +339,10 @@ class AnnotationCreation extends Component {
} = this.props;
const {
activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, strokeColor,
popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, annoBody, svg,
tstart, tend, textEditorStateBustingKey,
activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl,
strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode,
textBody, svg, tstart, tend,
textEditorStateBustingKey, image,
} = this.state;
const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined';
......@@ -326,6 +355,7 @@ class AnnotationCreation extends Component {
>
<AnnotationDrawing
activeTool={activeTool}
annotation={annotation}
fillColor={fillColor}
strokeColor={strokeColor}
strokeWidth={strokeWidth}
......@@ -440,7 +470,6 @@ class AnnotationCreation extends Component {
)
: null
}
</Grid>
</Grid>
<Grid container>
......@@ -464,7 +493,7 @@ class AnnotationCreation extends Component {
<Grid item xs={12}>
<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 />
</ToggleButton>
End
......@@ -481,14 +510,22 @@ class AnnotationCreation extends Component {
)}
<Grid item xs={12}>
<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>
</Grid>
<Grid item xs={12}>
<TextEditor
key={textEditorStateBustingKey}
annoHtml={annoBody}
updateAnnotationBody={this.updateBody}
annoHtml={textBody}
updateAnnotationBody={this.updateTextBody}
/>
</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 @@
export default class WebAnnotation {
/** */
constructor({
canvasId, id, fragsel, body, tags, svg, manifestId,
canvasId, id, fragsel, image, body, tags, svg, manifestId,
}) {
this.id = id;
this.canvasId = canvasId;
......@@ -10,6 +10,7 @@ export default class WebAnnotation {
this.body = body;
this.tags = tags;
this.svg = svg;
this.image = image;
this.manifestId = manifestId;
}
......@@ -27,12 +28,23 @@ export default class WebAnnotation {
/** */
createBody() {
let bodies = [];
if (this.body) {
bodies.push({
if (this.body && this.body.value !== '') {
const textBody = {
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) {
bodies = bodies.concat(this.tags.map((tag) => ({
purpose: 'tagging',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment