Skip to content
Snippets Groups Projects
Verified Commit 1d84b116 authored by Loïs Poujade's avatar Loïs Poujade
Browse files

Merge branch '2-update-from-upstream' into Add-img-annotation-insert-edit

parents 1cf3ea44 3d9ef3dc
Branches
No related tags found
No related merge requests found
......@@ -2,7 +2,7 @@
"env": {
"jest/globals": true
},
"extends": ["airbnb","react-app"],
"extends": ["airbnb"],
"globals": {
"page": true,
"document": true
......@@ -26,6 +26,8 @@
"sort-keys": ["error", "asc", {
"caseSensitive": false,
"natural": false
}]
}],
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-static-element-interactions": "off"
}
}
......@@ -10,7 +10,7 @@ function createWrapper(props) {
return shallow(
<AnnotationCreation
id="x"
config={{}}
config={{ annotation: {} }}
receiveAnnotation={jest.fn()}
windowId="abc"
{...props}
......
module.exports = {
plugins: [
// TODO loose: which options is ignored in depencies ?
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
],
presets: [
[
'@babel/preset-env',
......
......@@ -4,12 +4,12 @@ module.exports = {
type: 'react-component',
npm: {
esModules: true,
umd: {
global: 'MiradorAnnotation',
externals: {
react: 'React',
},
},
// umd: {
// global: 'MiradorAnnotation',
// externals: {
// react: 'React',
// },
// },
},
webpack: {
aliases: {
......
......@@ -11,8 +11,8 @@
"umd"
],
"scripts": {
"build": "nwb build-react-component",
"clean": "nwb clean-module && nwb clean-demo",
"build": "nwb build-react-component --no-demo",
"clean": "nwb clean-module",
"lint": "eslint ./src ./__tests__",
"prepublishOnly": "npm run build",
"start": "nwb serve-react-demo",
......@@ -21,12 +21,11 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@psychobolt/react-paperjs": "^1.0.0",
"@psychobolt/react-paperjs-editor": "^0.0.14",
"@psychobolt/react-paperjs": "< 1.0",
"@psychobolt/react-paperjs-editor": "0.0.11",
"draft-js": "^0.11.6",
"draft-js-export-html": "^1.4.1",
"draft-js-import-html": "^1.4.1",
"immutable": "^4.0.0-rc.12",
"material-ui-color-components": "^0.3.0",
"paper": "^0.12.11",
"react-color": "^2.18.1"
......@@ -38,8 +37,8 @@
"lodash": "^4.17.11",
"mirador": "^3.0.0-rc.5",
"prop-types": "^15.7.2",
"react": "^17.0",
"react-dom": "^17.0",
"react": "^16.8",
"react-dom": "^16.8",
"uuid": "^8.0.0"
},
"devDependencies": {
......@@ -49,27 +48,24 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.56",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.0",
"babel-eslint": "^10.1.0",
"canvas": "^2.6.1",
"enzyme": "^3.11.0",
"eslint": "^7.2",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.6.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jest": "^23.18.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^4.0.6",
"jest": "^26.1.0",
"jest-canvas-mock": "^2.2.0",
"jest-localstorage-mock": "^2.4.2",
"mirador": "^3.0.0-rc.5",
"nwb": "^0.24.7",
"prop-types": "^15.7.2",
"react": "^17.0",
"react-dom": "^17.0",
"react": "^16.8",
"react-dom": "^16.8",
"uuid": "^8.2.0"
},
"author": "",
......
import Enzyme from 'enzyme'; // eslint-disable-line import/no-extraneous-dependencies
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; // eslint-disable-line import/no-extraneous-dependencies
Enzyme.configure({ adapter: new Adapter() });
......@@ -39,6 +39,27 @@ import TextEditor from './TextEditor';
import WebAnnotation from './WebAnnotation';
import CursorIcon from './icons/Cursor';
/** Extract time information from annotation target */
function timeFromAnnoTarget(annotarget) {
// TODO w3c media fragments: t=,10 t=5,
const r = /t=([0-9]+),([0-9]+)/.exec(annotarget);
if (!r || r.length !== 3) {
return ['', ''];
}
return [r[1], r[2]];
}
/** Extract xywh from annotation target */
function geomFromAnnoTarget(annotarget) {
console.warn('TODO proper extraction');
const r = /xywh=((-?[0-9]+,?)+)/.exec(annotarget);
console.info('extracted from ', annotarget, r);
if (!r || r.length !== 3) {
return ['', ''];
}
return [r[1], r[2]];
}
/** */
class AnnotationCreation extends Component {
/** */
......@@ -67,6 +88,8 @@ class AnnotationCreation extends Component {
annoState.image = false;
if (props.annotation) {
//
// annotation body
if (Array.isArray(props.annotation.body)) {
annoState.tags = [];
props.annotation.body.forEach((body) => {
......@@ -85,28 +108,54 @@ class AnnotationCreation extends Component {
annoState.textBody = props.annotation.body.value;
annoState.image = props.annotation.body.image;
}
//
// drawing position
if (props.annotation.target.selector) {
if (Array.isArray(props.annotation.target.selector)) {
props.annotation.target.selector.forEach((selector) => {
if (selector.type === 'SvgSelector') {
annoState.svg = selector.value;
} else if (selector.type === 'FragmentSelector') {
annoState.xywh = selector.value.replace('xywh=', '');
// TODO proper fragment selector extraction
annoState.xywh = geomFromAnnoTarget(selector.value);
[annoState.tstart, annoState.tend] = timeFromAnnoTarget(selector.value);
}
});
} else {
annoState.svg = props.annotation.target.selector.value;
// eslint-disable-next-line max-len
[annoState.tstart, annoState.tend] = timeFromAnnoTarget(props.annotation.target.selector.value);
}
}
//
// start/end time
}
const toolState = {
activeTool: 'cursor',
closedMode: 'closed',
currentColorType: false,
fillColor: null,
strokeColor: '#00BFFF',
strokeWidth: 3,
...(props.config.annotation.defaults || {}),
};
this.state = {
...toolState,
annoBody: '',
textBody: '',
activeTool: 'cursor',
closedMode: 'closed',
colorPopoverOpen: false,
currentColorType: false,
fillColor: null,
lineWeightPopoverOpen: false,
openAddImgDialog: false,
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
svg: null,
tend: '',
tstart: '',
textEditorStateBustingKey: 0,
imgConstrain: false,
imgHeight: {
lastSubmittedValue: '',
......@@ -125,23 +174,18 @@ class AnnotationCreation extends Component {
validity: 0,
value: '',
},
lineWeightPopoverOpen: false,
openAddImgDialog: false,
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
strokeColor: '#00BFFF',
strokeWidth: 1,
svg: null,
textBody: '',
xywh: null,
...annoState,
};
this.submitForm = this.submitForm.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.updateGeometry = this.updateGeometry.bind(this);
this.changeTool = this.changeTool.bind(this);
this.changeClosedMode = this.changeClosedMode.bind(this);
......@@ -380,13 +424,18 @@ class AnnotationCreation extends Component {
submitForm(e) {
e.preventDefault();
const {
annotation, canvases, closeCompanionWindow, receiveAnnotation, config,
annotation, canvases, receiveAnnotation, config,
} = this.props;
const {
textBody, image, imgWidth, imgHeight, imgUrl, tags, xywh, svg, imgConstrain,
textBody, image, imgWidth, imgHeight, imgUrl, tags, xywh, svg,
imgConstrain,tstart, tend, textEditorStateBustingKey,
} = this.state;
const annoBody = { value: textBody };
let imgBody;
let fsel = xywh;
if (tstart && tend) {
fsel = `${xywh || ''}&t=${tstart},${tend}`;
}
if (imgWidth.validity === 1 && imgHeight.validity === 1 && imgUrl.validity === 1) {
imgBody = {
......@@ -410,7 +459,7 @@ class AnnotationCreation extends Component {
manifestId: canvas.options.resource.id,
svg,
tags,
xywh,
xywh: fsel,
}).toJson();
if (annotation) {
......@@ -423,10 +472,13 @@ class AnnotationCreation extends Component {
});
}
});
this.setState({
activeTool: null,
annoBody: '',
svg: null,
textEditorStateBustingKey: textEditorStateBustingKey + 1,
xywh: null,
});
closeCompanionWindow();
}
/** */
......@@ -448,6 +500,12 @@ class AnnotationCreation extends Component {
this.setState({ textBody });
}
/** update annotation start time */
updateTstart(ev) { this.setState({ tstart: ev.target.value }); }
/** update annotation end time */
updateTend(ev) { this.setState({ tend: ev.target.value }); }
/** */
updateGeometry({ svg, xywh }) {
this.setState({
......@@ -464,8 +522,9 @@ class AnnotationCreation extends Component {
const {
activeTool, colorPopoverOpen, currentColorType, fillColor, openAddImgDialog, popoverAnchorEl,
strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode,
textBody, imgUrl, imgWidth, imgHeight, imgConstrain, svg,
textBody, imgUrl, imgWidth, imgHeight, imgConstrain, svg, tstart, tend, textEditorStateBustingKey,
} = this.state;
return (
<CompanionWindow
title={annotation ? 'Edit annotation' : 'New annotation'}
......@@ -483,7 +542,7 @@ class AnnotationCreation extends Component {
updateGeometry={this.updateGeometry}
windowId={windowId}
/>
<form onSubmit={this.submitForm}>
<form onSubmit={this.submitForm} className={classes.section}>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
......@@ -591,6 +650,15 @@ class AnnotationCreation extends Component {
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Duration
</Typography>
</Grid>
<Grid item xs={12}>
<input name="tstart" type="number" step="1" value={tstart} onChange={this.updateTstart} />
<input name="tend" type="number" step="1" value={tend} onChange={this.updateTend} />
</Grid>
<Grid item xs={12}>
<Typography variant="overline">
Image Content
......@@ -675,6 +743,11 @@ class AnnotationCreation extends Component {
</Typography>
</Grid>
<Grid item xs={12}>
<TextEditor
key={textEditorStateBustingKey}
annoHtml={annoBody}
updateAnnotationBody={this.updateBody}
/>
<TextEditor
annoHtml={textBody}
updateAnnotationBody={this.updateTextBody}
......@@ -694,12 +767,15 @@ class AnnotationCreation extends Component {
>
<Paper>
<ClickAwayListener onClickAway={this.handleCloseLineWeight}>
<MenuList>
<MenuList autoFocus role="listbox">
{[1, 3, 5, 10, 50].map((option, index) => (
<MenuItem
key={option}
onClick={this.handleLineWeightSelect}
value={option}
selected={option == strokeWidth}
role="option"
aria-selected={option == strokeWidth}
>
{option}
</MenuItem>
......@@ -743,9 +819,16 @@ const styles = (theme) => ({
display: 'flex',
flexWrap: 'wrap',
},
section: {
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1),
paddingTop: theme.spacing(2),
},
});
AnnotationCreation.propTypes = {
// TODO proper web annotation type ?
annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types
canvases: PropTypes.arrayOf(
PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }),
......@@ -755,6 +838,11 @@ AnnotationCreation.propTypes = {
config: PropTypes.shape({
annotation: PropTypes.shape({
adapter: PropTypes.func,
defaults: PropTypes.objectOf(
PropTypes.oneOfType(
[PropTypes.bool, PropTypes.func, PropTypes.number, PropTypes.string]
)
),
}),
}).isRequired,
id: PropTypes.string.isRequired,
......
......@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
import { VideoViewersReferences } from 'mirador/dist/es/src/plugins/VideoViewersReferences';
import { renderWithPaperScope, PaperContainer } from '@psychobolt/react-paperjs';
import
{
......@@ -16,6 +17,17 @@ import flatten from 'lodash/flatten';
import EditTool from './EditTool';
import { mapChildren } from './utils';
/** Use a canvas "like a OSD viewport" (temporary) */
function viewportFromAnnotationOverlayVideo(annotationOverlayVideo) {
const { canvas } = annotationOverlayVideo;
return {
getCenter: () => ({ x: canvas.getWidth() / 2, y: canvas.getHeight() / 2 }),
getFlip: () => false,
getRotation: () => false,
getZoom: () => 1,
};
}
/** */
class AnnotationDrawing extends Component {
/** */
......@@ -25,12 +37,6 @@ class AnnotationDrawing extends Component {
this.addPath = this.addPath.bind(this);
}
/** */
componentDidMount() {
const { windowId } = this.props;
this.OSDReference = OSDReferences.get(windowId);
}
/** */
addPath(path) {
const { closed, strokeWidth, updateGeometry } = this.props;
......@@ -61,23 +67,32 @@ class AnnotationDrawing extends Component {
/** */
paperThing() {
const { windowId } = this.props;
let viewport = null;
let img = null;
if (OSDReferences.get(windowId)) {
console.debug('[annotation-plugin] OSD reference: ', OSDReferences.get(windowId));
viewport = OSDReferences.get(windowId).current.viewport;
img = OSDReferences.get(windowId).current.world.getItemAt(0);
} else if (VideoViewersReferences.get(windowId)) {
console.debug('[annotation-plugin] VideoViewers reference: ', VideoViewersReferences.get(windowId));
viewport = viewportFromAnnotationOverlayVideo(VideoViewersReferences.get(windowId).props);
}
const {
activeTool, fillColor, strokeColor, strokeWidth, svg,
} = this.props;
if (!activeTool || activeTool === 'cursor') return null;
// Setup Paper View to have the same center and zoom as the OSD Viewport
const viewportZoom = this.OSDReference.viewport.getZoom(true);
const image1 = this.OSDReference.world.getItemAt(0);
const center = image1.viewportToImageCoordinates(
this.OSDReference.viewport.getCenter(true),
);
const flipped = this.OSDReference.viewport.getFlip();
// Setup Paper View to have the same center and zoom as the OSD Viewport/video canvas
const center = img
? img.viewportToImageCoordinates(viewport.getCenter(true))
: viewport.getCenter();
const flipped = viewport.getFlip();
const viewProps = {
center: new Point(center.x, center.y),
rotation: this.OSDReference.viewport.getRotation(),
rotation: viewport.getRotation(),
scaling: new Point(flipped ? -1 : 1, 1),
zoom: image1.viewportToImageZoom(viewportZoom),
zoom: img ? img.viewportToImageZoom(viewport.getZoom()) : viewport.getZoom(),
};
let ActiveTool = RectangleTool;
......@@ -141,9 +156,12 @@ class AnnotationDrawing extends Component {
/** */
render() {
const { windowId } = this.props;
this.OSDReference = OSDReferences.get(windowId).current;
const container = OSDReferences.get(windowId)
? OSDReferences.get(windowId).current.element
: VideoViewersReferences.get(windowId).apiRef.current;
return (
ReactDOM.createPortal(this.paperThing(), this.OSDReference.element)
ReactDOM.createPortal(this.paperThing(), container)
);
}
}
......
......@@ -20,6 +20,16 @@ class TextEditor extends Component {
this.onChange = this.onChange.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.handleFormating = this.handleFormating.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.editorRef = React.createRef();
}
/**
* This is a kinda silly hack (but apparently recommended approach) to
* making sure the whole visible editor area is clickable, not just the first line.
*/
handleFocus() {
if (this.editorRef.current) this.editorRef.current.focus();
}
/** */
......@@ -58,6 +68,7 @@ class TextEditor extends Component {
const { classes } = this.props;
const { editorState } = this.state;
const currentStyle = editorState.getCurrentInlineStyle();
return (
<div>
<ToggleButtonGroup
......@@ -77,11 +88,13 @@ class TextEditor extends Component {
<ItalicIcon />
</ToggleButton>
</ToggleButtonGroup>
<div className={classes.editorRoot}>
<div className={classes.editorRoot} onClick={this.handleFocus}>
<Editor
editorState={editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange}
ref={this.editorRef}
/>
</div>
</div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment