diff --git a/.eslintrc b/.eslintrc index f3fe6398314c69b792d21eb4d7c8973e3bc0cc24..5690374c9b41a9efc6237332f9581e77fa8a522b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "env": { "jest/globals": true }, - "extends": ["airbnb","react-app"], + "extends": ["airbnb"], "globals": { "page": true, "document": true diff --git a/__tests__/WebAnnotation.test.js b/__tests__/WebAnnotation.test.js index 0e4baa8c3885e1ae568647cebf3f8bf72632f680..ee08252a386eac1bf4d26c2fb1f9b8462b0ad3c9 100644 --- a/__tests__/WebAnnotation.test.js +++ b/__tests__/WebAnnotation.test.js @@ -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,34 @@ 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: {url: 'http://example.photo/pic.jpg' }, tags: null }); + expect(subject.createBody()).toEqual([ + { + type: 'TextualBody', + value: 'hello', + }, + { + type: 'Image', + format: 'image/jpg', + id: 'http://example.photo/pic.jpg', + }, + ]); + }); }); describe('toJson', () => { it('generates a WebAnnotation', () => { diff --git a/package-lock.json b/package-lock.json index 9285c519542db25119becf7b9a4df2eb8c1b7932..8eeaafc622102e476b6dcc7a189e7cbb100b0ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5878,6 +5878,16 @@ "node": ">=0.10.0" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/blob": { "version": "0.0.5", "dev": true, @@ -6500,6 +6510,25 @@ "node": ">=0.10.0" } }, + "node_modules/chokidar/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/chokidar/node_modules/glob-parent": { "version": "3.1.0", "dev": true, @@ -10265,6 +10294,13 @@ "webpack": "^4.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "node_modules/filesize": { "version": "6.1.0", "dev": true, @@ -10698,6 +10734,20 @@ "dev": true, "license": "MIT" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "dev": true, @@ -15782,6 +15832,21 @@ "node": ">=6" } }, + "node_modules/mocha/node_modules/fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "deprecated": "\"Please update to latest v2.3 or v2.2\"", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/mocha/node_modules/glob": { "version": "7.1.3", "dev": true, @@ -28341,6 +28406,16 @@ "version": "1.13.1", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "blob": { "version": "0.0.5", "dev": true @@ -28805,6 +28880,17 @@ "to-regex-range": "^2.1.0" } }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, "glob-parent": { "version": "3.1.0", "dev": true, @@ -31465,6 +31551,13 @@ "schema-utils": "^2.5.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filesize": { "version": "6.1.0", "dev": true @@ -31779,6 +31872,13 @@ "version": "1.2.0", "dev": true }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "dev": true @@ -35188,6 +35288,13 @@ "locate-path": "^3.0.0" } }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, "glob": { "version": "7.1.3", "dev": true, diff --git a/src/AnnotationCreation.js b/src/AnnotationCreation.js index 49a04ec29b9b024d8f7a8d0f39ff45c0c8e00d2a..52042c62999a8e394004d4396526eea47bfcc22f 100644 --- a/src/AnnotationCreation.js +++ b/src/AnnotationCreation.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - IconButton, Button, Paper, Grid, Popover, Divider, + Button, Paper, Grid, Popover, Divider, MenuList, MenuItem, ClickAwayListener, } from '@material-ui/core'; import { Alarm, LastPage } from '@material-ui/icons'; @@ -19,6 +19,14 @@ 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 InsertPhotoIcon from '@material-ui/icons/InsertPhoto'; +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import TextField from '@material-ui/core/TextField'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; import { SketchPicker } from 'react-color'; import { v4 as uuid } from 'uuid'; import { withStyles } from '@material-ui/core/styles'; @@ -55,25 +63,52 @@ function geomFromAnnoTarget(annotarget) { /** */ class AnnotationCreation extends Component { + /** */ + static checkURL(url) { + const expression = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/; + const regex = new RegExp(expression); + + return url.match(regex); + } + + /** */ + static loadImg(url) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = url; + + img.onload = () => resolve({ height: img.height, width: img.width }); + img.onerror = reject; + }); + } + /** */ constructor(props) { super(props); const annoState = {}; + annoState.image = false; + 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 @@ -102,29 +137,55 @@ class AnnotationCreation extends Component { activeTool: 'cursor', closedMode: 'closed', currentColorType: false, + colorPopoverOpen: false, fillColor: null, strokeColor: '#00BFFF', strokeWidth: 3, ...(props.config.annotation.defaults || {}), }; - this.state = { ...toolState, - annoBody: '', - colorPopoverOpen: false, + textBody: '', + activeTool: 'cursor', + closedMode: 'closed', + currentColorType: false, + fillColor: null, lineWeightPopoverOpen: false, + openAddImgDialog: false, popoverAnchorEl: null, popoverLineWeightAnchorEl: null, svg: null, tend: Math.floor(props.currentTime) + 10, textEditorStateBustingKey: 0, tstart: Math.floor(props.currentTime), + imgConstrain: false, + imgHeight: { + lastSubmittedValue: '', + srcValue: '', + validity: 0, + value: '', + }, + imgUrl: { + lastSubmittedValue: '', + validity: 0, + value: '', + }, + imgWidth: { + lastSubmittedValue: '', + srcValue: '', + validity: 0, + value: '', + }, 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.getImgDimensions = this.getImgDimensions.bind(this); + this.setImgWidth = this.setImgWidth.bind(this); + this.setImgHeight = this.setImgHeight.bind(this); this.updateTstart = this.updateTstart.bind(this); this.updateTend = this.updateTend.bind(this); this.setTstartNow = this.setTstartNow.bind(this); @@ -140,6 +201,79 @@ class AnnotationCreation extends Component { this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this); this.closeChooseColor = this.closeChooseColor.bind(this); this.updateStrokeColor = this.updateStrokeColor.bind(this); + this.isConstrained = this.isConstrained.bind(this); + this.handleConstrainCheck = this.handleConstrainCheck.bind(this); + this.handleImgDialogChange = this.handleImgDialogChange.bind(this); + this.handleImgDialogSubmit = this.handleImgDialogSubmit.bind(this); + } + + /** */ + handleImgDialogChange(open) { + const { imgHeight, imgWidth, imgUrl } = this.state; + + this.setState({ + imgHeight: { + ...imgHeight, + validity: 1, + value: imgHeight.lastSubmittedValue, + }, + imgUrl: { + ...imgUrl, + validity: 1, + value: imgUrl.lastSubmittedValue, + }, + imgWidth: { + ...imgWidth, + validity: 1, + value: imgWidth.lastSubmittedValue, + }, + openAddImgDialog: open, + }); + } + + /** */ + handleConstrainCheck(event) { + const value = event.target.checked; + + this.setState({ + imgConstrain: value, + }); + } + + /** */ + handleImgDialogSubmit() { + let open = true; + const { imgUrl, imgHeight, imgWidth } = this.state; + + const urlValidity = AnnotationCreation.checkURL(imgUrl.value) ? 1 : 2; + const widthValidity = imgWidth.value > 0 ? 1 : 2; + const heightValidity = imgHeight.value > 0 ? 1 : 2; + if (urlValidity === 1 && widthValidity === 1 && heightValidity === 1) { + open = false; + } + + this.setState({ + image: { id: imgUrl.value }, + imgHeight: { + ...imgHeight, + lastSubmittedValue: heightValidity === 1 ? imgHeight.value : imgHeight.lastSubmittedValue, + validity: heightValidity, + value: imgHeight.value, + }, + imgUrl: { + ...imgUrl, + lastSubmittedValue: urlValidity === 1 ? imgUrl.value : imgUrl.lastSubmittedValue, + validity: urlValidity, + value: imgUrl.value, + }, + imgWidth: { + ...imgWidth, + lastSubmittedValue: widthValidity === 1 ? imgWidth.value : imgWidth.lastSubmittedValue, + validity: widthValidity, + value: imgWidth.value, + }, + openAddImgDialog: open, + }); } /** */ @@ -199,6 +333,106 @@ class AnnotationCreation extends Component { } } + /** */ + async getImgDimensions(url) { + const { imgHeight, imgWidth, imgUrl } = this.state; + const urlValidity = AnnotationCreation.checkURL(url) ? 1 : 2; + + try { + const dimensions = await AnnotationCreation.loadImg(url); + + this.setState({ + imgHeight: { + ...imgHeight, + srcValue: dimensions.height || '', + value: dimensions.height || '', + }, + imgUrl: { + ...imgUrl, + validity: 1, + value: url, + }, + imgWidth: { + ...imgWidth, + srcValue: dimensions.width || '', + value: dimensions.width || '', + }, + }); + } catch (e) { + this.setState({ + imgUrl: { + ...imgUrl, + validity: urlValidity, + value: url, + }, + }); + } + } + + /** */ + setImgWidth(value) { + const { imgWidth } = this.state; + this.setState({ + imgWidth: { + ...imgWidth, + value, + }, + }); + } + + /** */ + setImgUrl(value) { + const { imgUrl } = this.state; + this.setState({ + imgUrl: { + ...imgUrl, + value, + }, + }); + } + + /** */ + setImgHeight(value) { + const { imgHeight } = this.state; + this.setState({ + imgHeight: { + ...imgHeight, + value, + }, + }); + } + + /** */ + isConstrained(event) { // adjust other dimension in proportion to inputted dimension + const { imgConstrain, imgWidth, imgHeight } = this.state; + const ratio = imgWidth.srcValue / imgHeight.srcValue; + + if (imgConstrain) { + if (event.target.id === 'width' && imgWidth.srcValue !== '') { + // set height to be the same as width if width is less than 0 + const height = imgWidth.value > 0 ? imgWidth.value * (1 / ratio) : imgWidth.value; + this.setState({ + imgHeight: { + ...imgHeight, + validity: 1, + value: height, + }, + }); + } else if (event.target.id === 'height' && imgHeight.srcValue !== '') { + // set width to be the same as height if height is less than 0 + const width = imgHeight.value > 0 ? imgHeight.value * ratio : imgHeight.value; + + this.setState({ + imgWidth: { + ...imgWidth, + validity: 1, + value: width, + }, + }); + } + } + } + /** */ openChooseColor(e) { this.setState({ @@ -240,19 +474,37 @@ class AnnotationCreation extends Component { annotation, canvases, receiveAnnotation, config, } = this.props; const { - annoBody, tags, xywh, svg, tstart, tend, textEditorStateBustingKey, + textBody, image, imgWidth, imgHeight, imgUrl, tags, xywh, svg, + imgConstrain, tstart, tend, textEditorStateBustingKey, } = this.state; + const annoBody = { value: !textBody.length ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody }; + + let imgBody; + if (imgWidth.validity === 1 && imgHeight.validity === 1 && imgUrl.validity === 1) { + imgBody = { + constrain: imgConstrain, + h: imgHeight.value, + url: imgUrl.value, + w: imgWidth.value, + }; + } else { + imgBody = image; + } + canvases.forEach((canvas) => { const storageAdapter = config.annotation.adapter(canvas.id); + const anno = new WebAnnotation({ - body: !annoBody.length ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody, + body: annoBody, canvasId: canvas.id, fragsel: { t: `${tstart},${tend}`, xywh }, id: (annotation && annotation.id) || `${uuid()}`, + image: imgBody, manifestId: canvas.options.resource.id, svg, tags, }).toJson(); + if (annotation) { storageAdapter.update(anno).then((annoPage) => { receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); @@ -265,9 +517,10 @@ class AnnotationCreation extends Component { }); this.setState({ - annoBody: '', + image: false, svg: null, tend: 0, + textBody: '', textEditorStateBustingKey: textEditorStateBustingKey + 1, tstart: 0, xywh: null, @@ -289,8 +542,8 @@ class AnnotationCreation extends Component { } /** */ - updateBody(annoBody) { - this.setState({ annoBody }); + updateTextBody(textBody) { + this.setState({ textBody }); } /** */ @@ -307,9 +560,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, openAddImgDialog, popoverAnchorEl, + strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, + textBody, imgUrl, imgWidth, imgHeight, imgConstrain, svg, tstart, tend, + textEditorStateBustingKey, image, } = this.state; const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined'; @@ -322,6 +576,7 @@ class AnnotationCreation extends Component { > <AnnotationDrawing activeTool={activeTool} + annotation={annotation} fillColor={fillColor} strokeColor={strokeColor} strokeWidth={strokeWidth} @@ -436,47 +691,133 @@ class AnnotationCreation extends Component { ) : null } - </Grid> </Grid> <Grid container> { mediaIsVideo && ( <> - <Grid item xs={12} onClick={this.seekToTstart}> - <IconButton size="small"><LastPage /></IconButton> + <Grid item xs={12}> + <ToggleButton value="true" title="Go to start time" size="small" onClick={this.seekToTstart} className={classes.timecontrolsbutton}> + <LastPage /> + </ToggleButton> <Typography variant="overline"> Start </Typography> </Grid> <Grid item xs={12} className={classes.paper}> - <IconButton onClick={this.setTstartNow}><Alarm /></IconButton> + <ToggleButton value="true" title="Set current time" size="small" onClick={this.setTstartNow} className={classes.timecontrolsbutton}> + <Alarm /> + </ToggleButton> <HMSInput seconds={tstart} onChange={this.updateTstart} /> </Grid> - <Grid item xs={12} onClick={this.seekToTend}> + <Grid item xs={12}> <Typography variant="overline"> - <IconButton size="small"><LastPage /></IconButton> + <ToggleButton value="true" title="Go to end time" size="small" onClick={this.seekToTend} className={classes.timecontrolsbutton}> + <LastPage /> + </ToggleButton> End </Typography> </Grid> <Grid item xs={12} className={classes.paper}> - <IconButton onClick={this.setTendNow}><Alarm /></IconButton> + <ToggleButton value="true" title="Set current time" size="small" onClick={this.setTendNow} className={classes.timecontrolsbutton}> + <Alarm /> + </ToggleButton> <HMSInput seconds={tend} onChange={this.updateTend} /> </Grid> </> )} <Grid item xs={12}> <Typography variant="overline"> - Content + Image Content + </Typography> + </Grid> + <Grid item xs={12} style={{ marginBottom: 10 }}> + <ToggleButton value="image-icon" aria-label="insert an image" onClick={() => this.handleImgDialogChange(true)}> + { image === false && <InsertPhotoIcon /> } + { image !== false && <img src={image.id} width="100" height="auto" alt="no trad" /> } + </ToggleButton> + </Grid> + <Dialog open={openAddImgDialog} fullWidth onClose={() => this.handleImgDialogChange(false)} aria-labelledby="form-dialog-title"> + <DialogTitle id="form-dialog-title" disableTypography> + <Typography variant="h2">Insert image</Typography> + </DialogTitle> + <DialogContent> + <DialogTitle id="form-dialog-subtitle-1" style={{ paddingLeft: 0 }} disableTypography> + <Typography variant="h5">Image source</Typography> + </DialogTitle> + <TextField + value={imgUrl.value} + onChange={(e) => this.setImgUrl(e.target.value)} + onBlur={(e) => this.getImgDimensions(e.target.value)} + error={imgUrl.validity === 2} + helperText={imgUrl.validity === 2 ? 'Invalid URL' : ''} + margin="dense" + id="source" + label="Image URL" + type="url" + fullWidth + /> + </DialogContent> + <DialogContent> + <DialogTitle id="form-dialog-subtitle-2" style={{ paddingLeft: 0 }} disableTypography> + <Typography variant="h5">Image dimensions</Typography> + </DialogTitle> + <TextField + value={imgWidth.value} + style={{ marginRight: 10, width: 100 }} + onChange={(e) => this.setImgWidth(e.target.value)} + onBlur={(e) => this.isConstrained(e)} + error={imgWidth.validity === 2} + helperText={imgWidth.validity === 2 ? 'Invalid width' : ''} + margin="dense" + id="width" + label="Width" + type="number" + variant="outlined" + /> + <TextField + value={imgHeight.value} + style={{ marginLeft: 10, width: 100 }} + onChange={(e) => this.setImgHeight(e.target.value)} + onBlur={(e) => this.isConstrained(e)} + error={imgHeight.validity === 2} + helperText={imgHeight.validity === 2 ? 'Invalid height' : ''} + margin="dense" + id="height" + label="Height" + type="number" + variant="outlined" + /> + <FormControlLabel + control={( + <Checkbox + checked={imgConstrain} + onChange={(e) => this.handleConstrainCheck(e)} + inputProps={{ 'aria-label': 'primary checkbox' }} + style={{ marginLeft: 30 }} + /> + )} + label="Constrain proportions" + /> + </DialogContent> + <DialogActions> + <Button onClick={() => this.handleImgDialogChange(false)}>Cancel</Button> + <Button variant="contained" onClick={this.handleImgDialogSubmit} color="primary">Add</Button> + </DialogActions> + </Dialog> + <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> @@ -551,6 +892,13 @@ const styles = (theme) => ({ paddingRight: theme.spacing(1), paddingTop: theme.spacing(2), }, + timecontrolsbutton: { + height: '30px', + margin: 'auto', + marginLeft: '0', + marginRight: '5px', + width: '30px', + }, }); AnnotationCreation.propTypes = { diff --git a/src/WebAnnotation.js b/src/WebAnnotation.js index 281055edee98da07573e80af4880cd842cfbe389..84371660aba5e694e40c52b988b0d05b69e01e96 100644 --- a/src/WebAnnotation.js +++ b/src/WebAnnotation.js @@ -2,7 +2,7 @@ export default class WebAnnotation { /** */ constructor({ - canvasId, id, fragsel, body, tags, svg, manifestId, + canvasId, id, image, fragsel, 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,25 @@ 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 = { + // TODO format detection + format: 'image/jpg', + id: this.image.url, + type: 'Image', + }; + bodies.push(imgBody); + } + if (this.tags) { bodies = bodies.concat(this.tags.map((tag) => ({ purpose: 'tagging', diff --git a/src/plugins/annotationCreationCompanionWindow.js b/src/plugins/annotationCreationCompanionWindow.js index 5a1d537d60b399c09ad2e69d97b1c374118a2550..afe5bed4c5d67df92ac81225c52159f05344665d 100644 --- a/src/plugins/annotationCreationCompanionWindow.js +++ b/src/plugins/annotationCreationCompanionWindow.js @@ -2,6 +2,7 @@ import * as actions from 'mirador/dist/es/src/state/actions'; import { getCompanionWindow } from 'mirador/dist/es/src/state/selectors/companionWindows'; import { getWindowCurrentTime, getWindowPausedStatus } from 'mirador/dist/es/src/state/selectors/window'; import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases'; +import { getPresentAnnotationsOnSelectedCanvases } from 'mirador/dist/es/src/state/selectors/annotations'; import AnnotationCreation from '../AnnotationCreation'; /** */ @@ -23,16 +24,9 @@ function mapStateToProps(state, { id: companionWindowId, windowId }) { const { annotationid } = cw; const canvases = getVisibleCanvases(state, { windowId }); - let annotation = null; - canvases.forEach((canvas) => { - const annotationsOnCanvas = state.annotations[canvas.id]; - Object.values(annotationsOnCanvas || {}).forEach((value, i) => { - if (value.json && value.json.items) { - const maybeAnnot = value.json.items.find((anno) => anno.id === annotationid); - if (maybeAnnot !== undefined) annotation = maybeAnnot; - } - }); - }); + const annotation = getPresentAnnotationsOnSelectedCanvases(state, { windowId }) + .flatMap((annoPage) => annoPage.json.items) + .find((annot) => annot.id === annotationid); return { annotation,