diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000000000000000000000000000000000000..08a7276bba82164ee90215c65447ca52ad89825d --- /dev/null +++ b/.env.sample @@ -0,0 +1,14 @@ +# Use this variable to add configurations : +# docker-compose.yml : required +# ports.yml : bind the port 300 to the $PORT variable +# traefik.yml : add traefik configurations +COMPOSE_FILE=docker-compose.yml:docker/ports.yml +# Choose between "dev" and "prod" +ENV=dev +# If you use docker/ports.yml +PORT=3000 +# If you use docker/traefik.yml +# A unique name for traefik router +NAME= +# A traefik host rule ex `domain.FQDN` or `domain1.FQDN`,`domain2.FQDN` +HOST=`domain.fqdn` diff --git a/.gitignore b/.gitignore index 6a62d7fbef574204d29bed51a8681a9862b088ca..5a58408c7e16d3f21f5b20324300a871aa894b70 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ npm-debug.log* .idea .package-lock.json +.env +.*.sw? diff --git a/README.md b/README.md index 577585362ac1d65f3c1ec208b8ef285c4a5af804..ca3e8dda8a9b20a32359cd6b4148f024ca96eddf 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,6 @@ # mirador-annotations -## Install -``` -nvm use -git clone gitlab@gitlab.tetras-libre.fr:iiif/mirador/mirador-annotations.git -cd mirador-annotations/ -git checkout mui5/React17 -git clone gitlab@gitlab.tetras-libre.fr:iiif/mirador/mirador-video.git -rm -r mirador -mv mirador-video/ mirador -cd mirador/ -git checkout 3def696e -npm install -cd .. -npm install -``` [![Travis][build-badge]][build] [![npm package][npm-badge]][npm] @@ -25,6 +10,41 @@ npm install  +TODO Explain the evolution proposed by Tétras Libre fork + +## Install (local) + +This method requires `nvm`, `npm`. + +``` +git clone gitlab@gitlab.tetras-libre.fr:iiif/mirador/mirador-annotations.git +cd mirador-annotations +nvm use +npm install +``` +NPM Install throw two errors (https://gitlab.tetras-libre.fr/iiif/mirador/mirador-annotations/-/issues/12). To fix run : + +``` +./cli post_install +``` + +Run mirador and the plugin : + +``` +npm start +``` + +## Install using docker + +This method requires `docker` and `docker-compose` (or `docker compose`) + +``` +cp .env.sample .env +$EDITOR .env +# Change the variables you need +docker-compose up +``` + ## Persisting Annotations Persisting annotations requires implementing an a IIIF annotation server. Several [examples of annotation servers](https://github.com/IIIF/awesome-iiif#annotation-servers) are available on iiif-awesome. diff --git a/demo/src/index.js b/demo/src/index.js index f3a6642a1e5640f1eebf523fadea587264522f63..ae417e5d0c7d1463d1ba6a4e7333ef38413ac0c1 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -27,7 +27,6 @@ const config = { sideBarOpenByDefault: true, }, windows: [ - { manifestId: 'https://dzkimgs.l.u-tokyo.ac.jp/videos/iiif_in_japan_2017/manifest.json' }, ], }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..bb3118f9a673073e516b93fa7d0154346be67d4f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3" + +services: + front: + build: + context: "docker/" + volumes: + - ${PWD}:/app + environment: + ENV: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..73516dd7001876a0cae2d0aa5a885cca660161e1 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,16 @@ +FROM node:16 + +EXPOSE 3000 + +VOLUME /app + +WORKDIR /app + +RUN npm install -g serve + +COPY entrypoint.sh / + +USER node + +ENTRYPOINT ["/entrypoint.sh"] + diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..486fd1d9de0d9fbd89e610f5a57918fe218ccf82 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +npm install +./cli post_install + +if [ "$ENV" == "prod" ]; then + npm run build + cmd="serve -s demo/dist" +else + cmd="npm start" +fi + +if [ ! -z "$1" ]; then + cmd=$@ +fi +exec $cmd diff --git a/docker/ports.yml b/docker/ports.yml new file mode 100644 index 0000000000000000000000000000000000000000..c7334bd2295bc0f9a0e163da83cb5a99709d573f --- /dev/null +++ b/docker/ports.yml @@ -0,0 +1,6 @@ +version: "3" + +services: + front: + ports: + - ${PORT}:3000 diff --git a/docker/traefik.yml b/docker/traefik.yml new file mode 100644 index 0000000000000000000000000000000000000000..a0e02e3d53aa2b2ea5880d8000dde0277ce9a7d3 --- /dev/null +++ b/docker/traefik.yml @@ -0,0 +1,19 @@ +version: '3' + +services: + front: + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.${NAME}.rule=Host(${HOST})" + - "traefik.http.routers.${NAME}.tls.certresolver=myresolver" + - "traefik.http.routers.${NAME}.entrypoints=web,websecure" + - "traefik.http.routers.${NAME}.middlewares=hardening@docker" + + +networks: + traefik: + external: true + diff --git a/package.json b/package.json index 0d6c5d21764845f7c4f0f9093b5f6376acdf88cc..75405869ce86b22a2a39e59aac56af9a264f6b4c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react-resize-observer": "^1.1.1", "react-sortablejs": "^6.1.4", "sortablejs": "^1.15.2", + "redux": "^4.2.1", "use-image": "^1.1.1" }, "peerDependencies": { diff --git a/src/AnnotationCreation.js b/src/AnnotationCreation.js index b26498c2bd0a580d935fe2d9178cb7295fd21239..b6ac753a479185f3f099a5fe8c573c0dedb6d4aa 100644 --- a/src/AnnotationCreation.js +++ b/src/AnnotationCreation.js @@ -1,14 +1,20 @@ import React, { useEffect, useLayoutEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { - Button, ClickAwayListener, Divider, Grid, MenuItem, MenuList, Paper, Popover, + Button, } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { v4 as uuid, v4 as uuidv4 } from 'uuid'; +import { v4 as uuid } from 'uuid'; import { exportStageSVG } from 'react-konva-to-svg'; import CompanionWindow from 'mirador/dist/es/src/containers/CompanionWindow'; import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences'; import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences'; +import Tab from '@mui/material/Tab'; +import HighlightAltIcon from '@mui/icons-material/HighlightAlt'; +import LayersIcon from '@mui/icons-material/Layers'; +import LocalOfferIcon from '@mui/icons-material/LocalOffer'; +import HubIcon from '@mui/icons-material/Hub'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; import AnnotationDrawing from './annotationForm/AnnotationDrawing'; import WebAnnotation from './WebAnnotation'; import { secondsToHMS } from './utils'; @@ -17,10 +23,19 @@ import AnnotationFormTime from './annotationForm/AnnotationFormTime'; import AnnotationFormDrawing from './annotationForm/AnnotationFormDrawing'; import { geomFromAnnoTarget, timeFromAnnoTarget } from './AnnotationCreationUtils'; +const TARGET_VIEW = 'target'; +const OVERLAY_VIEW = 'layer'; +const TAG_VIEW = 'tag'; +const MANIFEST_LINK_VIEW = 'link'; + /** Component for creating annotations. * Display in companion window when a manifest is open and an annoation created or edited */ function AnnotationCreation(props) { + + + + const [toolState, setToolState] = useState({ activeTool: 'cursor', closedMode: 'closed', @@ -32,12 +47,14 @@ function AnnotationCreation(props) { lineWeightPopoverOpen: false, popoverAnchorEl: null, popoverLineWeightAnchorEl: null, - strokeColor: 'rgba(255, 0, 0, 1)', + strokeColor: 'green', strokeWidth: 3, }); // Initial state setup const [state, setState] = useState(() => { + let tstart; + let tend; const annoState = {}; if (props.annotation) { // annotation body @@ -71,7 +88,7 @@ function AnnotationCreation(props) { } else if (selector.type === 'FragmentSelector') { // TODO proper fragment selector extraction annoState.xywh = geomFromAnnoTarget(selector.value); - [annoState.tstart, annoState.tend] = timeFromAnnoTarget(selector.value); + [tstart, tend] = timeFromAnnoTarget(selector.value); } }); } else { @@ -80,20 +97,25 @@ function AnnotationCreation(props) { } } else if (typeof props.annotation.target === 'string') { annoState.xywh = geomFromAnnoTarget(props.annotation.target); - [annoState.tstart, annoState.tend] = timeFromAnnoTarget(props.annotation.target); + [tstart, tend] = timeFromAnnoTarget(props.annotation.target); } } - const timeState = props.currentTime !== null - ? { tend: Math.floor(props.currentTime) + 10, tstart: Math.floor(props.currentTime) } - : { tend: null, tstart: null }; + // If we dont have tstart setted, we are creating a new annotation. + // So Tstart is current time and Tend the end of the video + if (!tstart) { + tstart = props.currentTime ? Math.floor(props.currentTime) : 0; + tend = tstart + 30; + } return { ...toolState, - ...timeState, mediaVideo: null, ...annoState, + tend, textEditorStateBustingKey: 0, + tstart, + valueTime: [0, 1], valuetextTime: '', valueTime: [0, 1], }; @@ -105,7 +127,9 @@ function AnnotationCreation(props) { const { height, width } = VideosReferences.get(props.windowId).ref.current; + const [value, setValue] = useState('layer'); + // TODO Check the effect to keep and remove the other // Add a state to trigger redraw const [windowSize, setWindowSize] = useState({ width: window.innerWidth, @@ -139,18 +163,6 @@ function AnnotationCreation(props) { useLayoutEffect(() => { }, [{ height, width }]); - // You can use useEffect for componentDidMount, componentDidUpdate, and componentWillUnmount - useEffect(() => { - // componentDidMount logic - const mediaVideo = VideosReferences.get(props.windowId); - setState((prevState) => ({ ...prevState, mediaVideo })); - - // componentWillUnmount logic (if needed) - return () => { - // cleanup logic here - }; - }, []); // Empty array means this effect runs once, similar to componentDidMount - /** */ const handleImgChange = (newUrl, imgRef) => { setToolState((prevState) => ({ @@ -184,26 +196,34 @@ function AnnotationCreation(props) { valueTime: newValueTime, })); }; - + const tabHandler = (event, TabIndex) => { + setValue(TabIndex); + }; /** - * @param {Event} event - * @param {number} newValueTime - */ + * Change from slider + * @param {Event} event + * @param {number} newValueTime + */ const handleChangeTime = (event, newValueTime) => { const timeStart = newValueTime[0]; const timeEnd = newValueTime[1]; updateTstart(timeStart); updateTend(timeEnd); seekToTstart(); - seekToTend(); setValueTime(newValueTime); }; - /** update annotation start time */ + /** Change from Tstart HMS Input */ const updateTstart = (value) => { + if (value > state.tend) { + return; + } setState((prevState) => ({ ...prevState, tstart: value, + ...props.setSeekTo(value), + ...props.setCurrentTime(value), + })); }; @@ -215,23 +235,6 @@ function AnnotationCreation(props) { })); }; - /** update annotation title */ - const updateTitle = (e) => { - setState((prevState) => ({ - ...prevState, - title: e.target.value, - })); - }; - - /** seekTo/goto annotation end time */ - const seekToTend = () => { - setState((prevState) => ({ - ...prevState, - ...props.setSeekTo(prevState.tend), - ...props.setCurrentTime(prevState.tend), - })); - }; - // eslint-disable-next-line require-jsdoc const seekToTstart = () => { setState((prevState) => ({ @@ -286,6 +289,7 @@ function AnnotationCreation(props) { return svg; }; + /** Set color tool from current shape */ const setColorToolFromCurrentShape = (colorState) => { setToolState((prevState) => ({ @@ -298,7 +302,7 @@ function AnnotationCreation(props) { /** update shapes with shapes from annotationDrawing */ const updateShapes = (newShapes) => { - + setShapes(newShapes); } @@ -408,7 +412,6 @@ function AnnotationCreation(props) { tend, textEditorStateBustingKey, valueTime, - title, } = state; const { @@ -440,6 +443,7 @@ function AnnotationCreation(props) { console.debug('osdref', osdref); } + /** Change scale from container / canva */ const updateScale = () => { setScale(overlay.containerWidth / overlay.canvasWidth); }; @@ -447,24 +451,14 @@ function AnnotationCreation(props) { useEffect(() => { }, [overlay.containerWidth, overlay.canvasWidth]); - - return ( // we need to get the width and height of the image to pass it to the annotation drawing component <CompanionWindow - title={title ? title.value : 'New Annotation'} + title={annotation ? 'Edit annotation' : 'New annotation'} windowId={windowId} id={id} > - <AnnotationDrawing - style={{ - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: 'auto', - - }} + <StyledAnnotationDrawing scale={scale} activeTool={activeTool} annotation={annotation} @@ -491,42 +485,71 @@ function AnnotationCreation(props) { <StyledForm onSubmit={submitForm} > - <AnnotationFormContent - onChange={updateTitle} - textBody={state.textBody} - textEditorStateBustingKey={textEditorStateBustingKey} - updateTextBody={updateTextBody} - /> - {mediaIsVideo && ( - <AnnotationFormTime - mediaIsVideo={mediaIsVideo} - videoDuration={videoDuration} - value={valueTime} - handleChangeTime={handleChangeTime} - windowid={windowId} - setTstartNow={setTstartNow} - tstart={tstart} - updateTstart={updateTstart} - setTendNow={setTendNow} - tend={tend} - updateTend={updateTend} + <TabContext value={value}> + <TabList value={value} onChange={tabHandler} aria-label="icon tabs"> + <StyledTab + icon={<HighlightAltIcon />} + aria-label="TargetSelector" + value={TARGET_VIEW} + /> + <StyledTab + icon={<LayersIcon />} + aria-label="TargetSelector" + value={OVERLAY_VIEW} + /> + <StyledTab + icon={<LocalOfferIcon />} + aria-label="TargetSelector" + value={TAG_VIEW} + /> + <StyledTab + icon={<HubIcon />} + aria-label="TargetSelector" + value={MANIFEST_LINK_VIEW} + /> + </TabList> + <StyledTabPanel + value={TARGET_VIEW} + > + {mediaIsVideo && ( + <AnnotationFormTime + mediaIsVideo={mediaIsVideo} + videoDuration={videoDuration} + value={valueTime} + handleChangeTime={handleChangeTime} + windowid={windowId} + setTstartNow={setTstartNow} + tstart={tstart} + updateTstart={updateTstart} + setTendNow={setTendNow} + tend={tend} + updateTend={updateTend} + /> + )} + </StyledTabPanel> + <StyledTabPanel + value={OVERLAY_VIEW} + > + <AnnotationFormDrawing + toolState={toolState} + updateToolState={setToolState} + handleImgChange={handleImgChange} + /> + </StyledTabPanel> + <StyledTabPanel + value={TAG_VIEW} + > + <AnnotationFormContent + textBody={textBody} + updateTextBody={updateTextBody} + textEditorStateBustingKey={textEditorStateBustingKey} + /> + </StyledTabPanel> + <StyledTabPanel + value={MANIFEST_LINK_VIEW} /> - )} - <AnnotationFormDrawing - toolState={toolState} - updateToolState={setToolState} - handleImgChange={handleImgChange} - /> - <ul id='layerlist'> - {Array.isArray(shapes) && shapes.length > 0 && shapes.map((shape) => ( - <li key={shape.id}> - {shape.id} - <button onClick={() => deleteShape(shape.id)}>Delete</button> - </li> - ))} - </ul> - - <div> + </TabContext> + <StyledButtonDivSaveOrCancel> <Button onClick={closeCompanionWindow}> Cancel </Button> @@ -536,13 +559,17 @@ function AnnotationCreation(props) { > Save </Button> - - </div> + </StyledButtonDivSaveOrCancel> </StyledForm> </CompanionWindow> ); } +const StyledButtonDivSaveOrCancel = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'flex-end', +})); + const StyledForm = styled('form')(({ theme }) => ({ display: 'flex', flexDirection: 'column', @@ -553,6 +580,23 @@ const StyledForm = styled('form')(({ theme }) => ({ paddingTop: theme.spacing(2), })); +const StyledTab = styled(Tab)(({ theme }) => ({ + minWidth: '0px', + padding: '12px 8px', +})); + +const StyledTabPanel = styled(TabPanel)(({ theme }) => ({ + padding: '0', +})); + +const StyledAnnotationDrawing = styled(AnnotationDrawing)(({ theme }) => ({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: 'auto', +})); + AnnotationCreation.propTypes = { // TODO proper web annotation type ? annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types diff --git a/src/CanvasListItem.js b/src/CanvasListItem.js index 97402eba93350643cdd8adb6f724b68f23990b2b..6d9238c7208369b6ab84770293d6fc54e1069bc4 100644 --- a/src/CanvasListItem.js +++ b/src/CanvasListItem.js @@ -1,4 +1,6 @@ -import React, { Component, createRef, forwardRef } from 'react'; +import React, { + useState, useContext, forwardRef, useEffect, +} from 'react'; import PropTypes from 'prop-types'; import DeleteIcon from '@mui/icons-material/DeleteForever'; import EditIcon from '@mui/icons-material/Edit'; @@ -7,43 +9,34 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import flatten from 'lodash/flatten'; import AnnotationActionsContext from './AnnotationActionsContext'; -/** */ -class CanvasListItem extends Component { - /** */ - constructor(props) { - super(props); +const CanvasListItem = forwardRef((props, ref) => { + const [isHovering, setIsHovering] = useState(false); + const context = useContext(AnnotationActionsContext); - this.state = { - isHovering: false, - }; + const handleMouseHover = () => { + setIsHovering(!isHovering); + }; - this.handleMouseHover = this.handleMouseHover.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.handleEdit = this.handleEdit.bind(this); - } - - /** */ - handleDelete() { - const { canvases, receiveAnnotation, storageAdapter } = this.context; - const { annotationid } = this.props; + const handleDelete = () => { + const { canvases, receiveAnnotation, storageAdapter } = context; + const { annotationid } = props; canvases.forEach((canvas) => { const adapter = storageAdapter(canvas.id); adapter.delete(annotationid).then((annoPage) => { receiveAnnotation(canvas.id, adapter.annotationPageId, annoPage); }); }); - } + }; - /** */ - handleEdit() { + const handleEdit = () => { const { addCompanionWindow, canvases, annotationsOnCanvases, - } = this.context; - const { annotationid } = this.props; + } = context; + const { annotationid } = props; let annotation; canvases.some((canvas) => { if (annotationsOnCanvases[canvas.id]) { - Object.entries(annotationsOnCanvases[canvas.id]).forEach(([key, value], i) => { + Object.entries(annotationsOnCanvases[canvas.id]).forEach(([key, value]) => { if (value.json && value.json.items) { annotation = value.json.items.find((anno) => anno.id === annotationid); } @@ -55,22 +48,14 @@ class CanvasListItem extends Component { annotationid, position: 'right', }); - } - - /** */ - handleMouseHover() { - this.setState((prevState) => ({ - isHovering: !prevState.isHovering, - })); - } + }; - /** */ - editable() { - const { annotationsOnCanvases, canvases } = this.context; - const { annotationid } = this.props; + const editable = () => { + const { annotationsOnCanvases, canvases } = context; + const { annotationid } = props; const annoIds = canvases.map((canvas) => { if (annotationsOnCanvases[canvas.id]) { - return flatten(Object.entries(annotationsOnCanvases[canvas.id]).map(([key, value], i) => { + return flatten(Object.entries(annotationsOnCanvases[canvas.id]).map(([key, value]) => { if (value.json && value.json.items) { return value.json.items.map((item) => item.id); } @@ -80,58 +65,55 @@ class CanvasListItem extends Component { return []; }); return flatten(annoIds).includes(annotationid); - } + }; - /** */ - render() { - const { isHovering } = this.state; - const { windowViewType, toggleSingleCanvasDialogOpen } = this.context; - - return ( - <div - onMouseEnter={this.handleMouseHover} - onMouseLeave={this.handleMouseHover} - className="mirador-annotation-list-item" - > - {isHovering && this.editable() && ( - <div - style={{ - position: 'relative', - top: -20, - zIndex: 10000, - }} + return ( + <div + onMouseEnter={handleMouseHover} + onMouseLeave={handleMouseHover} + className="mirador-annotation-list-item" + ref={ref} + > + {isHovering && editable() && ( + <div + style={{ + position: 'relative', + top: -20, + zIndex: 10000, + }} + > + <ToggleButtonGroup + aria-label="annotation tools" + size="small" + style={{ position: 'absolute', right: 0 }} + disabled={!context.annotationEditCompanionWindowIsOpened} > - <ToggleButtonGroup - aria-label="annotation tools" - size="small" - style={{ - position: 'absolute', - right: 0, - }} + <ToggleButton + aria-label="Edit" + onClick={context.windowViewType === 'single' ? handleEdit : context.toggleSingleCanvasDialogOpen} + value="edit" > - <ToggleButton - aria-label="Edit" - onClick={windowViewType === 'single' ? this.handleEdit : toggleSingleCanvasDialogOpen} - value="edit" - > - <EditIcon /> - </ToggleButton> - <ToggleButton aria-label="Delete" onClick={this.handleDelete} value="delete"> - <DeleteIcon /> - </ToggleButton> - </ToggleButtonGroup> - </div> - )} - <li - {...this.props} // eslint-disable-line react/jsx-props-no-spreading - > - </li> - </div> - ); - } -} + <EditIcon /> + </ToggleButton> + <ToggleButton + aria-label="Delete" + onClick={handleDelete} + value="delete" + > + <DeleteIcon /> + </ToggleButton> + </ToggleButtonGroup> + </div> + )} + <li {...props}> + {props.children} + </li> + </div> + ); +}); CanvasListItem.propTypes = { + annotationEditCompanionWindowIsOpened: PropTypes.bool.isRequired, annotationid: PropTypes.string.isRequired, children: PropTypes.oneOfType([ PropTypes.func, @@ -139,6 +121,4 @@ CanvasListItem.propTypes = { ]).isRequired, }; -CanvasListItem.contextType = AnnotationActionsContext; - -export default forwardRef((props, ref) => <CanvasListItem {...props} containerRef={ref} />); +export default CanvasListItem; diff --git a/src/HMSInput.js b/src/HMSInput.js index bd69395d308d802c556fb76b75a3bafc4e5f117a..93695eba39aa39a9895ec029ca270c152579a783 100644 --- a/src/HMSInput.js +++ b/src/HMSInput.js @@ -5,14 +5,6 @@ import { secondsToHMSarray } from './utils'; const StyledInput = styled(Input)(({ theme }) => ({ height: 'fit-content', - margin: '2px', - '& input[type=number]': { - 'MozAppearance': 'textfield', - }, - '& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button': { - 'WebkitAppearance': 'none', - margin: 0, - }, })); const StyledHMSLabel = styled('span')({ @@ -24,19 +16,24 @@ const StyledRoot = styled('div')({ display: 'flex', }); +/** Hours minutes seconds form inputs */ function HMSInput({ seconds, onChange }) { - const [hms, setHms] = useState(secondsToHMSarray(0)); + const [hms, setHms] = useState(secondsToHMSarray(seconds)); useEffect(() => { - if(seconds != null) { - setHms(secondsToHMSarray(seconds)); - } + if (seconds != null) { + setHms(secondsToHMSarray(Number(seconds))); + } }, [seconds]); + /** Handle change on one form */ const someChange = (ev) => { - const newState = secondsToHMSarray(Number(ev.target.value)); - setHms(newState); - onChange(newState.hours * 3600 + newState.minutes * 60 + newState.seconds); + if (!ev.target.value) { + return; + } + hms[ev.target.name] = Number(ev.target.value); + setHms(hms); + onChange(hms.hours * 3600 + hms.minutes * 60 + hms.seconds); }; return ( @@ -48,9 +45,11 @@ function HMSInput({ seconds, onChange }) { name="hours" value={hms.hours} onChange={someChange} - inputProps={{ style: { textAlign: 'center' } }} + dir="rtl" + inputProps={{ style: { width: '35px' } }} + /> - <StyledHMSLabel>h</StyledHMSLabel> + <StyledHMSLabel style={{ margin: '2px' }}>h</StyledHMSLabel> <StyledInput type="number" min="0" @@ -58,9 +57,10 @@ function HMSInput({ seconds, onChange }) { name="minutes" value={hms.minutes} onChange={someChange} - inputProps={{ style: { textAlign: 'center' } }} + dir="rtl" + inputProps={{ style: { width: '35px' } }} /> - <StyledHMSLabel>m</StyledHMSLabel> + <StyledHMSLabel style={{ margin: '2px' }}>m</StyledHMSLabel> <StyledInput type="number" min="0" @@ -68,16 +68,17 @@ function HMSInput({ seconds, onChange }) { name="seconds" value={hms.seconds} onChange={someChange} - inputProps={{ style: { textAlign: 'center' } }} + dir="rtl" + inputProps={{ style: { width: '35px' } }} /> - <StyledHMSLabel>s</StyledHMSLabel> + <StyledHMSLabel style={{ margin: '2px' }}>s</StyledHMSLabel> </StyledRoot> ); } HMSInput.propTypes = { onChange: PropTypes.func.isRequired, - seconds: PropTypes.number, + seconds: PropTypes.number.isRequired, }; export default HMSInput; diff --git a/src/SingleCanvasDialog.js b/src/SingleCanvasDialog.js index 9e3ad9f8f4a278382b3a4d234d199f5c60920e42..b63c2ca4621bd4c45e4df1c13587c929a18e1524 100644 --- a/src/SingleCanvasDialog.js +++ b/src/SingleCanvasDialog.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; /** * Dialog to enforce single view for annotation creation / editing */ -class SingleCanvasDialog extends Component { +class SingleCanvasDialog extends Component { /** */ constructor(props) { super(props); diff --git a/src/TextEditor.js b/src/TextEditor.js index 1c929989d5b86b27e50c31d7fcb95df5afc60f47..f8a27805a4c9000cc137bf7eb681dbced69b76a7 100644 --- a/src/TextEditor.js +++ b/src/TextEditor.js @@ -1,21 +1,15 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import ReactQuill from 'react-quill'; -import 'react-quill/dist/quill.snow.css'; // include styles -import { styled } from '@mui/system'; +import 'react-quill/dist/quill.snow.css'; +import {styled} from "@mui/material/styles"; +import {Paper} from "@mui/material"; // include styles - - -const EditorRoot = styled('div')(({ theme }) => ({ - borderColor: theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)', - borderRadius: theme.shape.borderRadius, - borderStyle: 'solid', - borderWidth: 1, - marginBottom: theme.spacing(1), - marginTop: theme.spacing(1), - padding: theme.spacing(1), +const StyledReactQuill = styled(ReactQuill)(({ theme }) => ({ + ".ql-editor":{ + minHeight:'150px' +} })); - function TextEditor({ annoHtml, updateAnnotationBody }) { const [editorHtml, setEditorHtml] = useState(annoHtml); @@ -27,13 +21,13 @@ function TextEditor({ annoHtml, updateAnnotationBody }) { }; return ( - <EditorRoot> - <ReactQuill + <div> + <StyledReactQuill value={editorHtml} onChange={handleChange} // You can also pass other props to customize the toolbar, etc. /> - </EditorRoot> + </div> ); } diff --git a/src/annotationForm/AnnotationFormContent.js b/src/annotationForm/AnnotationFormContent.js index 69784ac4d751162ce7c27d3a4b2a785d3b5c14cf..642e44befd943f9196bd02755d6fa0a6831f299a 100644 --- a/src/annotationForm/AnnotationFormContent.js +++ b/src/annotationForm/AnnotationFormContent.js @@ -1,34 +1,25 @@ import React from 'react'; -import { Grid, TextField, Typography } from '@mui/material'; - +import {Grid, Paper, Typography} from '@mui/material'; import PropTypes from 'prop-types'; import TextEditor from '../TextEditor'; /** Form part for edit annotation content and body */ -function AnnotationFormContent({ - onChange, textBody, updateTextBody, textEditorStateBustingKey, +function AnnotationFormContent({textBody, updateTextBody, textEditorStateBustingKey, }) { return ( - <div> + <Paper style={{padding:"5px"}}> <Typography variant="overline"> - Content + Metadata </Typography> - <Grid item xs={12}> - <TextField - id="outlined-basic" - label="Title" - variant="outlined" - onChange={onChange} - /> - </Grid> - <Grid> + <Grid + > <TextEditor key={textEditorStateBustingKey} annoHtml={textBody} updateAnnotationBody={updateTextBody} /> </Grid> - </div> + </Paper> ); } diff --git a/src/annotationForm/AnnotationFormDrawing.js b/src/annotationForm/AnnotationFormDrawing.js index 00aafa6acd61c4179c556f44db511e892a2bd315..7496d03055038fe0251fd4657fcc31c877e785ef 100644 --- a/src/annotationForm/AnnotationFormDrawing.js +++ b/src/annotationForm/AnnotationFormDrawing.js @@ -4,15 +4,14 @@ import { import Typography from '@mui/material/Typography'; import ToggleButton from '@mui/material/ToggleButton'; import TitleIcon from '@mui/icons-material/Title'; -import FormatShapesIcon from '@mui/icons-material/FormatShapes'; -import AccessibilityNewIcon from '@mui/icons-material/AccessibilityNew'; +import ImageIcon from '@mui/icons-material/Image'; +import DeleteIcon from '@mui/icons-material/Delete'; import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; import RectangleIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CircleIcon from '@mui/icons-material/RadioButtonUnchecked'; import PolygonIcon from '@mui/icons-material/Timeline'; -import DeleteIcon from '@mui/icons-material/Delete'; import GestureIcon from '@mui/icons-material/Gesture'; -import React, {useEffect, useState} from 'react'; +import React, { useEffect } from 'react'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import StrokeColorIcon from '@mui/icons-material/BorderColor'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; @@ -25,9 +24,9 @@ import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; import { SketchPicker } from 'react-color'; import { v4 as uuidv4 } from 'uuid'; +import CategoryIcon from '@mui/icons-material/Category'; import CursorIcon from '../icons/Cursor'; import ImageFormField from './ImageFormField'; -import { fill } from 'lodash'; const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({ '&:first-of-type': { @@ -44,28 +43,35 @@ const StyledDivider = styled(Divider)(({ theme }) => ({ margin: theme.spacing(1, 0.5), })); +const StyledPaper = styled(Paper)(({ theme }) => ({ + padding: '5px', +})); -const rgbaToObj = (rgba='rgba(255,255,255,0.5)') => { - - - - +const StyledDivButtonImage = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'flex-end', + marginTop: '5px', +})); +const rgbaToObj = (rgba = 'rgba(255,255,255,0.5)') => { const rgbaArray = rgba.split(','); const r = Number(rgbaArray[0].split('(')[1]); const g = Number(rgbaArray[1]); const b = Number(rgbaArray[2]); const a = Number(rgbaArray[3].split(')')[0]); - return { r, g, b, a }; -} + return { + // eslint-disable-next-line sort-keys + r, g, b, a, + }; +}; -const objToRgba = (obj={r:255,g:255,b:255,a:0.5}) => { - return `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`; -} +const objToRgba = (obj = { + // eslint-disable-next-line sort-keys + r: 255, g: 255, b: 255, a: 0.5, +}) => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`; /** All the stuff to manage to choose the drawing tool */ function AnnotationFormDrawing({ updateToolState, toolState, handleImgChange }) { - useEffect(() => { }, [toolState.fillColor, toolState.strokeColor, toolState.strokeWidth]); @@ -91,16 +97,13 @@ function AnnotationFormDrawing({ updateToolState, toolState, handleImgChange }) /** Update color : fillColor or strokeColor */ const updateStrokeColor = (color) => { - updateToolState({ ...toolState, [toolState.currentColorType]: objToRgba(color.rgb), }); - }; /** */ const openChooseColor = (e) => { - updateToolState({ ...toolState, colorPopoverOpen: true, @@ -172,147 +175,160 @@ function AnnotationFormDrawing({ updateToolState, toolState, handleImgChange }) currentColorType, } = toolState; - - return ( - <div> + <StyledPaper> <div> <Grid container> <Grid item xs={12}> <Typography variant="overline"> - Drawing + Overlay </Typography> </Grid> <Grid item xs={12}> - <Paper - elevation={0} - sx={{ - display: 'flex', - flexWrap: 'wrap', - }} - > - <StyledToggleButtonGroup - value={activeTool} // State or props ? - exclusive - onChange={changeTool} - aria-label="tool selection" - size="small" - > - <ToggleButton value="text" aria-label="select text"> - <TitleIcon /> - </ToggleButton> - <ToggleButton value="cursor" aria-label="select cursor"> - <CursorIcon /> - </ToggleButton> - <ToggleButton value="edit" aria-label="select cursor"> - <FormatShapesIcon /> - </ToggleButton> - <ToggleButton value="debug" aria-label="select cursor"> - <AccessibilityNewIcon /> - </ToggleButton> - </StyledToggleButtonGroup> - <StyledDivider - flexItem - orientation="vertical" - /> - <StyledToggleButtonGroup - value={activeTool} // State or props ? - exclusive - onChange={changeTool} - aria-label="tool selection" - size="small" - > - <ToggleButton value="arrow" aria-label="add an arrow"> - <ArrowOutwardIcon /> - </ToggleButton> - <ToggleButton value="rectangle" aria-label="add a rectangle"> - <RectangleIcon /> - </ToggleButton> - <ToggleButton value="ellipse" aria-label="add a circle"> - <CircleIcon /> - </ToggleButton> - <ToggleButton value="polygon" aria-label="add a polygon"> - <PolygonIcon /> - </ToggleButton> - <ToggleButton value="freehand" aria-label="free hand polygon"> - <GestureIcon /> - </ToggleButton> - <ToggleButton value="delete" aria-label="delete a shape"> - <DeleteIcon /> - </ToggleButton> - </StyledToggleButtonGroup> - </Paper> - </Grid> - </Grid> - </div> - <div> - <Grid container> - <Grid item xs={12}> - <Typography variant="overline"> - Style - </Typography> - </Grid> - <Grid item xs={12}> - <ToggleButtonGroup - aria-label="style selection" + <StyledToggleButtonGroup + value={activeTool} // State or props ? + exclusive + onChange={changeTool} + aria-label="tool selection" size="small" > - <ToggleButton - value="strokeColor" - aria-label="select color" - onClick={openChooseColor} - > - <StrokeColorIcon style={{ fill: strokeColor }} /> - <ArrowDropDownIcon /> + + <ToggleButton value="edit" aria-label="select cursor"> + <CursorIcon /> </ToggleButton> - <ToggleButton - value="strokeColor" - aria-label="select line weight" - onClick={openChooseLineWeight} - > - <LineWeightIcon /> - <ArrowDropDownIcon /> + <ToggleButton value="shapes" aria-label="select cursor"> + <CategoryIcon /> </ToggleButton> - <ToggleButton - value="fillColor" - aria-label="select color" - - onClick={openChooseColor} - > - <FormatColorFillIcon style={{ fill: fillColor }} /> - <ArrowDropDownIcon /> + <ToggleButton value="images" aria-label="select cursor"> + <ImageIcon /> + </ToggleButton> + <ToggleButton value="text" aria-label="select text"> + <TitleIcon /> + </ToggleButton> + <ToggleButton value="delete" aria-label="select cursor"> + <DeleteIcon /> </ToggleButton> - </ToggleButtonGroup> + </StyledToggleButtonGroup> + { + activeTool === 'edit' && ( + <Typography> + Liste des shapes pour Sam + </Typography> + ) + } + { + activeTool === 'shapes' ? ( + <> + <StyledToggleButtonGroup + value={activeTool} // State or props ? + exclusive + onChange={changeTool} + aria-label="tool selection" + size="small" + > + <ToggleButton value="rectangle" aria-label="add a rectangle"> + <RectangleIcon /> + </ToggleButton> + <ToggleButton value="ellipse" aria-label="add a circle"> + <CircleIcon /> + </ToggleButton> + <ToggleButton value="arrow" aria-label="add an arrow"> + <ArrowOutwardIcon /> + </ToggleButton> + <ToggleButton value="polygon" aria-label="add a polygon"> + <PolygonIcon /> + </ToggleButton> + <ToggleButton value="freehand" aria-label="free hand polygon"> + <GestureIcon /> + </ToggleButton> + <ToggleButton value="delete" aria-label="delete a shape"> + <DeleteIcon /> + </ToggleButton> + </StyledToggleButtonGroup> + <div> + <Grid container> + <Grid item xs={12}> + <Typography variant="overline"> + Style + </Typography> + </Grid> + <Grid item xs={12}> + <ToggleButtonGroup + aria-label="style selection" + size="small" + > + <ToggleButton + value="strokeColor" + aria-label="select color" + onClick={openChooseColor} + > + <StrokeColorIcon style={{ fill: strokeColor }} /> + <ArrowDropDownIcon /> + </ToggleButton> + <ToggleButton + value="strokeColor" + aria-label="select line weight" + onClick={openChooseLineWeight} + > + <LineWeightIcon /> + <ArrowDropDownIcon /> + </ToggleButton> + <ToggleButton + value="fillColor" + aria-label="select color" + onClick={openChooseColor} + > + <FormatColorFillIcon style={{ fill: fillColor }} /> + <ArrowDropDownIcon /> + </ToggleButton> + </ToggleButtonGroup> - <StyledDivider flexItem orientation="vertical" /> - { /* close / open polygon mode only for freehand drawing mode. */ - activeTool === 'freehand' - ? ( - <ToggleButtonGroup - size="small" - value={closedMode} - onChange={changeClosedMode} - > - <ToggleButton value="closed"> - <ClosedPolygonIcon /> - </ToggleButton> - <ToggleButton value="open"> - <OpenPolygonIcon /> - </ToggleButton> - </ToggleButtonGroup> - ) - : null - } - </Grid> - </Grid> - <Grid container> - <Grid item xs={8} style={{ marginBottom: 10 }}> - <ImageFormField xs={8} value={image} onChange={handleImgChange} /> - </Grid> - <Grid item xs={4} style={{ marginBottom: 10 }}> - <Button variant="contained" onClick={addImage}> - <AddPhotoAlternateIcon /> - </Button> + <StyledDivider flexItem orientation="vertical" /> + { /* close / open polygon mode only for freehand drawing mode. */ + activeTool === 'freehand' + ? ( + <ToggleButtonGroup + size="small" + value={closedMode} + onChange={changeClosedMode} + > + <ToggleButton value="closed"> + <ClosedPolygonIcon /> + </ToggleButton> + <ToggleButton value="open"> + <OpenPolygonIcon /> + </ToggleButton> + </ToggleButtonGroup> + ) + : null + } + </Grid> + </Grid> + </div> + </> + ) : (<></>) + } + { + activeTool === 'images' ? ( + <> + <Grid container> + <ImageFormField xs={8} value={image} onChange={handleImgChange} /> + </Grid> + <StyledDivButtonImage> + <Button variant="contained" onClick={addImage}> + <AddPhotoAlternateIcon /> + </Button> + </StyledDivButtonImage> + </> + ) : (<></>) + } + { + activeTool === 'text' && ( + <Typography> + Ajouter un input text + </Typography> + ) + } </Grid> </Grid> </div> @@ -350,7 +366,7 @@ function AnnotationFormDrawing({ updateToolState, toolState, handleImgChange }) onChangeComplete={updateStrokeColor} /> </Popover> - </div> + </StyledPaper> ); } diff --git a/src/annotationForm/AnnotationFormTime.js b/src/annotationForm/AnnotationFormTime.js index b4683f2f7cbb16334ad62c1e2fe7f495669a4925..554c392d3e022637d16a304d995b56cddf1ef628 100644 --- a/src/annotationForm/AnnotationFormTime.js +++ b/src/annotationForm/AnnotationFormTime.js @@ -1,26 +1,74 @@ -import { Grid } from '@mui/material'; +import { Divider, Grid, Paper } from '@mui/material'; import Typography from '@mui/material/Typography'; import Slider from '@mui/material/Slider'; import ToggleButton from '@mui/material/ToggleButton'; import { Alarm } from '@mui/icons-material'; import React from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import HMSInput from '../HMSInput'; +const StyledPaper = styled(Paper)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: '10px', + padding: '5px', +})); + +const StyledSlider = styled(Slider)(({ theme }) => ({ + color: 'rgba(1, 0, 0, 0.38)', +})); + +const StyledDivFormTimeContainer = styled('div')(({ theme }) => ({ + alignContent: 'center', + display: 'flex', + flexDirection: 'column', + gap: '5px', +})); +const StyledDivTimeSelector = styled('div')(({ theme }) => ({ + border: '1px solid rgba(0, 0, 0, 0.12)', + borderRadius: '4px', + display: 'flex', + flexWrap: 'nowrap', + justifyContent: 'center', + padding: '5px', +})); + +const StyledDivToggleButton = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', +})); + +const StyledLabelSelector = styled('p')(({ theme }) => ({ + fontSize: '15px', + margin: 0, + minWidth: '40px', +})); + +const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({ + border: 'none', + height: '30px', + margin: 'auto', + marginLeft: '0', + marginRight: '5px', +})); + +/** Form part with time mangement, dual slider + double input. Mange Tstart and Tend value */ function AnnotationFormTime({ videoDuration, value, handleChangeTime, windowid, setTstartNow, tstart, updateTstart, setTendNow, tend, updateTend, ...props }) { return ( - <> + <StyledPaper> <Grid item xs={12} > <Typography id="range-slider" variant="overline"> - Display period + Target </Typography> <div> - <Slider + <StyledSlider + size="small" value={value} onChange={handleChangeTime} valueLabelDisplay="auto" @@ -28,107 +76,48 @@ function AnnotationFormTime({ max={Math.round(videoDuration)} color="secondary" windowid={windowid} - sx={{ - color: 'rgba(1, 0, 0, 0.38)', - }} /> </div> </Grid> - <div style={{ - alignContent: 'center', - display: 'flex', - flexDirection: 'column', - gap: '5px', - padding: '5px', - }} - > - <div style={{ - border: '1px solid rgba(0, 0, 0, 0.12)', - borderRadius: '4px', - display: 'flex', - flexWrap: 'nowrap', - justifyContent: 'center', - padding: '5px', - }} - > - <div style={{ - display: 'flex', - flexDirection: 'column', - }} - > + <StyledDivFormTimeContainer> + <StyledDivTimeSelector> + <StyledDivToggleButton> <div> - <p style={{ - fontSize: '15px', - margin: 0, - minWidth: '40px', - }} - > + <StyledLabelSelector> Start - </p> + </StyledLabelSelector> </div> - <ToggleButton + <StyledToggleButton value="true" title="Set current time" size="small" onClick={setTstartNow} - style={{ - border: 'none', - height: '30px', - margin: 'auto', - marginLeft: '0', - marginRight: '5px', - }} > <Alarm fontSize="small" /> - </ToggleButton> - </div> + </StyledToggleButton> + </StyledDivToggleButton> <HMSInput seconds={tstart} onChange={updateTstart} /> - </div> - <div style={{ - border: '1px solid rgba(0, 0, 0, 0.12)', - borderRadius: '4px', - display: 'flex', - flexWrap: 'nowrap', - justifyContent: 'center', - padding: '5px', - }} - > - <div style={{ - display: 'flex', - flexDirection: 'column', - }} - > + </StyledDivTimeSelector> + <StyledDivTimeSelector> + <StyledDivToggleButton> <div> - <p style={{ - fontSize: '15px', - margin: 0, - minWidth: '40px', - }} - > + <StyledLabelSelector> End - </p> + </StyledLabelSelector> </div> - <ToggleButton + <StyledToggleButton value="true" title="Set current time" size="small" onClick={setTendNow} - style={{ - border: 'none', - height: '30px', - margin: 'auto', - marginLeft: '0', - marginRight: '5px', - }} > <Alarm fontSize="small" /> - </ToggleButton> - </div> + </StyledToggleButton> + </StyledDivToggleButton> <HMSInput seconds={tend} onChange={updateTend} /> - </div> - </div> - - </> + </StyledDivTimeSelector> + </StyledDivFormTimeContainer> + </StyledPaper> ); } diff --git a/src/annotationForm/ImageFormField.js b/src/annotationForm/ImageFormField.js index b5583a644eb2e0b9c83a632349e417f7b6a8376f..0606ffe9ef8f965a287f148076a37a7b779fc927 100644 --- a/src/annotationForm/ImageFormField.js +++ b/src/annotationForm/ImageFormField.js @@ -9,6 +9,11 @@ const StyledRoot = styled('div')(({ theme }) => ({ justifyContent: 'center', })); +const StyledTextField = styled(TextField)(({ theme }) => ({ + marginTop:"0", + marginBottom:"0", +})); + function ImageFormField({ value: image, onChange }) { const inputRef = useRef(null); const [imgIsValid, setImgIsValid] = useState(false); @@ -25,7 +30,7 @@ function ImageFormField({ value: image, onChange }) { return ( <StyledRoot> - <TextField + <StyledTextField value={imgUrl} onChange={(ev) => onChange(ev.target.value)} error={imgUrl !== '' && !imgIsValid} diff --git a/src/containers/miradorAnnotationPlugin.js b/src/containers/miradorAnnotationPlugin.js new file mode 100644 index 0000000000000000000000000000000000000000..60847d6846609dfd921a9226aa91c60a685d5f86 --- /dev/null +++ b/src/containers/miradorAnnotationPlugin.js @@ -0,0 +1,28 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { getWindowViewType } from 'mirador/dist/es/src/state/selectors'; +import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases'; +import { getCompanionWindowsForContent } from 'mirador/dist/es/src/state/selectors/companionWindows'; +import MiradorAnnotation from '../plugins/miradorAnnotationPlugin'; + +// TODO use selector in main componenent +function mapStateToProps(state, { targetProps: { windowId } }) { + // Annotation edit companion window ou annotation creation companion window is the same thing + const annotationCreationCompanionWindows = getCompanionWindowsForContent(state, { content: 'annotationCreation', windowId }); + let annotationEditCompanionWindowIsOpened = true; + if (Object.keys(annotationCreationCompanionWindows).length !== 0) { + annotationEditCompanionWindowIsOpened = false; + } + return { + annotationEditCompanionWindowIsOpened, + canvases: getVisibleCanvases(state, { windowId }), + config: state.config, + windowViewType: getWindowViewType(state, { windowId }), + }; +} + +const enhance = compose( + connect(mapStateToProps), +); + +export default enhance(MiradorAnnotation); diff --git a/src/index.js b/src/index.js index efa1720ca4d8050cdb0d3701c48cd6e03ce60fe8..063d2924179a239ad3a477bb0686e61371de74c4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ -import miradorAnnotationPlugin from './plugins/miradorAnnotationPlugin'; +// import miradorAnnotationPlugin from './plugins/miradorAnnotationPlugin'; +import miradorAnnotationPlugin from './containers/miradorAnnotationPlugin'; import externalStorageAnnotationPlugin from './plugins/externalStorageAnnotationPlugin'; import canvasAnnotationsPlugin from './plugins/canvasAnnotationsPlugin'; import annotationCreationCompanionWindow from './plugins/annotationCreationCompanionWindow'; @@ -11,7 +12,10 @@ export { }; export default [ - miradorAnnotationPlugin, + {component:miradorAnnotationPlugin, + mode: 'wrap', + target: 'AnnotationSettings', + }, externalStorageAnnotationPlugin, canvasAnnotationsPlugin, annotationCreationCompanionWindow, diff --git a/src/plugins/canvasAnnotationsPlugin.js b/src/plugins/canvasAnnotationsPlugin.js index bc919160967ba044c17020a7e604184435c53ea8..79fe2e579b50ff0cfaa9c3733189590c38c825b2 100644 --- a/src/plugins/canvasAnnotationsPlugin.js +++ b/src/plugins/canvasAnnotationsPlugin.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases'; import * as actions from 'mirador/dist/es/src/state/actions'; import { getWindowViewType } from 'mirador/dist/es/src/state/selectors'; +import { getCompanionWindowsForContent } from 'mirador/dist/es/src/state/selectors/companionWindows'; import CanvasListItem from '../CanvasListItem'; import AnnotationActionsContext from '../AnnotationActionsContext'; import SingleCanvasDialog from '../SingleCanvasDialog'; @@ -30,19 +31,18 @@ class CanvasAnnotationsWrapper extends Component { render() { const { addCompanionWindow, annotationsOnCanvases, canvases, config, receiveAnnotation, - switchToSingleCanvasView, TargetComponent, targetProps, windowViewType, containerRef, + switchToSingleCanvasView, TargetComponent, targetProps, windowViewType, containerRef, annotationEditCompanionWindowIsOpened, } = this.props; const { singleCanvasDialogOpen } = this.state; - const props = { ...targetProps, listContainerComponent: CanvasListItem, }; - return ( <AnnotationActionsContext.Provider value={{ addCompanionWindow, + annotationEditCompanionWindowIsOpened, annotationsOnCanvases, canvases, config, @@ -70,10 +70,15 @@ class CanvasAnnotationsWrapper extends Component { } CanvasAnnotationsWrapper.propTypes = { + TargetComponent: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.node, + ]).isRequired, addCompanionWindow: PropTypes.func.isRequired, - annotationsOnCanvases: PropTypes.object, // eslint-disable-line react/forbid-prop-types + annotationEditCompanionWindowIsOpened: PropTypes.bool.isRequired, // eslint-disable-line react/forbid-prop-types + annotationsOnCanvases: PropTypes.object, canvases: PropTypes.arrayOf( - PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }), + PropTypes.shape({id: PropTypes.string, index: PropTypes.number}), ), config: PropTypes.shape({ annotation: PropTypes.shape({ @@ -82,14 +87,10 @@ CanvasAnnotationsWrapper.propTypes = { }).isRequired, containerRef: PropTypes.oneOfType([ PropTypes.func, - PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + PropTypes.shape({current: PropTypes.instanceOf(Element)}), ]), receiveAnnotation: PropTypes.func.isRequired, switchToSingleCanvasView: PropTypes.func.isRequired, - TargetComponent: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.node, - ]).isRequired, targetProps: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types windowViewType: PropTypes.string.isRequired, }; @@ -100,10 +101,16 @@ CanvasAnnotationsWrapper.defaultProps = { containerRef: null, }; -/** */ +/** TODO this logic is duplicated */ function mapStateToProps(state, { targetProps: { windowId } }) { const canvases = getVisibleCanvases(state, { windowId }); const annotationsOnCanvases = {}; + const annotationCreationCompanionWindows = getCompanionWindowsForContent(state, { content: 'annotationCreation', windowId }); + let annotationEditCompanionWindowIsOpened = true; + + if (Object.keys(annotationCreationCompanionWindows).length !== 0) { + annotationEditCompanionWindowIsOpened = false; + } canvases.forEach((canvas) => { const anno = state.annotations[canvas.id]; @@ -112,6 +119,7 @@ function mapStateToProps(state, { targetProps: { windowId } }) { } }); return { + annotationEditCompanionWindowIsOpened, annotationsOnCanvases, canvases, config: state.config, @@ -120,7 +128,7 @@ function mapStateToProps(state, { targetProps: { windowId } }) { } /** */ -const mapDispatchToProps = (dispatch, props) => ({ +const mapDispatchToProps = (dispatch, props, annotationEditCompanionWindowIsOpened) => ({ addCompanionWindow: (content, additionalProps) => dispatch( actions.addCompanionWindow(props.targetProps.windowId, { content, ...additionalProps }), ), diff --git a/src/plugins/miradorAnnotationPlugin.js b/src/plugins/miradorAnnotationPlugin.js index eb02b09a468228e76d07272ee1ab9dadd97c9522..50b7313933c719964d22485fd9b35d6f4fbdb688 100644 --- a/src/plugins/miradorAnnotationPlugin.js +++ b/src/plugins/miradorAnnotationPlugin.js @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, {useState, useCallback, useEffect} from 'react'; import PropTypes from 'prop-types'; import AddBoxIcon from '@mui/icons-material/AddBox'; import GetAppIcon from '@mui/icons-material/GetApp'; @@ -11,21 +11,21 @@ import SingleCanvasDialog from '../SingleCanvasDialog'; import AnnotationExportDialog from '../AnnotationExportDialog'; import LocalStorageAdapter from '../LocalStorageAdapter'; -/** */ -function MiradorAnnotation({ targetProps, TargetComponent }) { +/** Mirador annotation plugin component. Get all the stuff and info to manage annotation functionnality */ +function MiradorAnnotation({ targetProps, TargetComponent, annotationEditCompanionWindowIsOpened}) { const [annotationExportDialogOpen, setAnnotationExportDialogOpen] = useState(false); const [singleCanvasDialogOpen, setSingleCanvasDialogOpen] = useState(false); const [currentCompanionWindowId, setCurrentCompanionWindowId] = useState(null); const dispatch = useDispatch(); - /** Open the companion window for annotation */ const addCompanionWindow = (content, additionalProps) => { - console.log(targetProps.windowId); setCurrentCompanionWindowId(targetProps.windowId); dispatch(actions.addCompanionWindow(targetProps.windowId, { content, ...additionalProps })); }; + useEffect(() => { + }, [annotationEditCompanionWindowIsOpened]); /** */ const switchToSingleCanvasView = () => { dispatch(actions.setWindowViewType(targetProps.windowId, 'single')); @@ -42,6 +42,7 @@ function MiradorAnnotation({ targetProps, TargetComponent }) { }, [targetProps.windowId]); const toggleSingleCanvasDialogOpen = useCallback(() => { + setSingleCanvasDialogOpen(!singleCanvasDialogOpen); }, [singleCanvasDialogOpen]); @@ -60,6 +61,7 @@ function MiradorAnnotation({ targetProps, TargetComponent }) { aria-label="Create new annotation" onClick={windowViewType === 'single' ? openCreateAnnotationCompanionWindow : toggleSingleCanvasDialogOpen} size="small" + disabled={!annotationEditCompanionWindowIsOpened} > <AddBoxIcon /> </MiradorMenuButton> @@ -92,25 +94,23 @@ function MiradorAnnotation({ targetProps, TargetComponent }) { } MiradorAnnotation.propTypes = { + TargetComponent: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.node, + ]).isRequired, + annotationEditCompanionWindowIsOpened: PropTypes.bool.isRequired, canvases: PropTypes.arrayOf( - PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }), + PropTypes.shape({id: PropTypes.string, index: PropTypes.number}), ).isRequired, config: PropTypes.shape({ annotation: PropTypes.shape({ adapter: PropTypes.func, exportLocalStorageAnnotations: PropTypes.bool, }), - }).isRequired, - TargetComponent: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.node, - ]).isRequired, - targetProps: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + }).isRequired, // eslint-disable-line react/forbid-prop-types + createAnnotation: PropTypes.bool.isRequired, + targetProps: PropTypes.object.isRequired, windowViewType: PropTypes.string.isRequired, }; -export default { - component: MiradorAnnotation, - mode: 'wrap', - target: 'AnnotationSettings', -}; +export default MiradorAnnotation;