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
No related branches found
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';
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