diff --git a/src/AnnotationCreation.js b/src/AnnotationCreation.js index a76463e59fecd085848380a6573ffae33557e377..062ac07549a97889d457ff53c7febf7c5a8c424c 100644 --- a/src/AnnotationCreation.js +++ b/src/AnnotationCreation.js @@ -20,12 +20,12 @@ import LineWeightIcon from '@mui/icons-material/LineWeight'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import FormatShapesIcon from '@mui/icons-material/FormatShapes'; import { SketchPicker } from 'react-color'; +import { styled } from '@mui/material/styles'; import { v4 as uuid } from 'uuid'; -import { styled } from '@mui/system'; +import Slider from '@mui/material/Slider'; 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 Slider from '@mui/material/Slider'; import AnnotationDrawing from './AnnotationDrawing'; import TextEditor from './TextEditor'; import WebAnnotation from './WebAnnotation'; @@ -225,7 +225,11 @@ class AnnotationCreation extends Component { /** seekTo/goto annotation end time */ seekToTend() { - const { paused, setCurrentTime, setSeekTo } = this.props; + const { + paused, + setCurrentTime, + setSeekTo, + } = this.props; const { tend } = this.state; if (!paused) { this.setState(setSeekTo(tend)); @@ -236,7 +240,11 @@ class AnnotationCreation extends Component { // eslint-disable-next-line require-jsdoc seekToTstart() { - const { paused, setCurrentTime, setSeekTo } = this.props; + const { + paused, + setCurrentTime, + setSeekTo, + } = this.props; const { tstart } = this.state; if (!paused) { this.setState(setSeekTo(tstart)); @@ -288,10 +296,20 @@ class AnnotationCreation extends Component { submitForm(e) { e.preventDefault(); const { - annotation, canvases, receiveAnnotation, config, + annotation, + canvases, + receiveAnnotation, + config, } = this.props; const { - textBody, image, tags, xywh, svg, tstart, tend, textEditorStateBustingKey, + textBody, + image, + tags, + xywh, + svg, + tstart, + tend, + textEditorStateBustingKey, } = this.state; const t = (tstart && tend) ? `${tstart},${tend}` : null; const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody }; @@ -302,7 +320,10 @@ class AnnotationCreation extends Component { const anno = new WebAnnotation({ body, canvasId: canvas.id, - fragsel: { t, xywh }, + fragsel: { + t, + xywh, + }, id: (annotation && annotation.id) || `${uuid()}`, image, manifestId: canvas.options.resource.id, @@ -311,13 +332,15 @@ class AnnotationCreation extends Component { }).toJson(); if (annotation) { - storageAdapter.update(anno).then((annoPage) => { - receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); - }); + storageAdapter.update(anno) + .then((annoPage) => { + receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); + }); } else { - storageAdapter.create(anno).then((annoPage) => { - receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); - }); + storageAdapter.create(anno) + .then((annoPage) => { + receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); + }); } }); @@ -352,23 +375,43 @@ class AnnotationCreation extends Component { } /** */ - updateGeometry({ svg, xywh }) { + updateGeometry({ + svg, + xywh, + }) { this.setState({ - svg, xywh, + svg, + xywh, }); } /** */ render() { const { - annotation, classes, closeCompanionWindow, id, windowId, + annotation, + closeCompanionWindow, + id, + windowId, } = this.props; const { - activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, - strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, - textBody, svg, tstart, tend, - textEditorStateBustingKey, image, valueTime, + activeTool, + colorPopoverOpen, + currentColorType, + fillColor, + popoverAnchorEl, + strokeColor, + popoverLineWeightAnchorEl, + lineWeightPopoverOpen, + strokeWidth, + closedMode, + textBody, + svg, + tstart, + tend, + textEditorStateBustingKey, + image, + valueTime, } = this.state; let mediaVideo; @@ -398,7 +441,9 @@ class AnnotationCreation extends Component { windowId={windowId} player={mediaIsVideo ? VideosReferences.get(windowId) : OSDReferences.get(windowId)} /> - <form onSubmit={this.submitForm} className={classes.section}> + <StyledForm + onSubmit={this.submitForm} + > <div> <Grid item xs={12}> <Typography variant="overline"> @@ -416,77 +461,140 @@ class AnnotationCreation extends Component { <div> {mediaIsVideo && ( - <> - <Grid item xs={12} className={classes.paper}> - <Typography id="range-slider" variant="overline"> - Display period - </Typography> - {/* <Typography> + <> + <Grid + item + xs={12} + sx={{ + display: 'flex', + flexWrap: 'wrap', + }} + > + <Typography id="range-slider" variant="overline"> + Display period + </Typography> + {/* <Typography> {mediaIsVideo ? mediaVideo?.video.duration : null} </Typography> */} - <Slider - value={valueTime} - onChange={this.handleChangeTime} - valueLabelDisplay="auto" - aria-labelledby="range-slider" - getAriaValueText={secondsToHMS} - max={mediaVideo ? mediaVideo.video.duration : null} - color="secondary" - windowId={windowId} - classes={{ - root: classes.MuiSliderColorSecondary, + <Slider + value={valueTime} + onChange={this.handleChangeTime} + valueLabelDisplay="auto" + aria-labelledby="range-slider" + getAriaValueText={secondsToHMS} + max={mediaVideo ? mediaVideo.video.duration : null} + color="secondary" + windowId={windowId} + sx={{ + color: 'rgba(1, 0, 0, 0.38)', + }} + /> + </Grid> + <div style={{ + alignContent: 'center', + display: 'flex', + flexDirection: 'wrap', + 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', }} - /> - </Grid> - <div className={`${classes.paper} ${classes.selectTimeField} `}> - <div className={`${classes.paper} ${classes.selectTimeModule} `}> - <div className={classes.buttonTimeContainer}> - <div> - <p className={classes.textTimeButton}>Start</p> - </div> - <ToggleButton - value="true" - title="Set current time" - size="small" - onClick={this.setTstartNow} - className={classes.timecontrolsbutton} + > + <div style={{ + display: 'flex', + flexDirection: 'column', + }} > - <Alarm fontSize="small" /> - </ToggleButton> - </div> - <HMSInput seconds={tstart} onChange={this.updateTstart} /> - </div> - <div className={`${classes.paper} ${classes.selectTimeModule}`}> - <div className={classes.buttonTimeContainer}> - <div> - <p className={classes.textTimeButton}>End</p> + <div> + <p style={{ + fontSize: '15px', + margin: 0, + minWidth: '40px', + }} + > + Start + </p> + </div> + <ToggleButton + value="true" + title="Set current time" + size="small" + onClick={this.setTstartNow} + style={{ + border: 'none', + height: '30px', + margin: 'auto', + marginLeft: '0', + marginRight: '5px', + }} + > + <Alarm fontSize="small" /> + </ToggleButton> </div> - <ToggleButton - value="true" - title="Set current time" - size="small" - onClick={this.setTendNow} - className={classes.timecontrolsbutton} + {/* <HMSInput seconds={tstart} onChange={this.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', + }} > - <Alarm fontSize="small" /> - </ToggleButton> + <div> + <p style={{ + fontSize: '15px', + margin: 0, + minWidth: '40px', + }} + > + End + </p> + </div> + <ToggleButton + value="true" + title="Set current time" + size="small" + onClick={this.setTendNow} + style={{ + border: 'none', + height: '30px', + margin: 'auto', + marginLeft: '0', + marginRight: '5px', + }} + > + <Alarm fontSize="small" /> + </ToggleButton> + </div> + {/* <HMSInput seconds={tend} onChange={this.updateTend} /> */} </div> - <HMSInput seconds={tend} onChange={this.updateTend} /> </div> - </div> - </> + </> )} </div> <div> <Grid container> - <Grid item xs={12}> <Typography variant="overline"> Image Content </Typography> </Grid> <Grid item xs={12} style={{ marginBottom: 10 }}> - <ImageFormField value={image} onChange={this.handleImgChange} /> + {/* <ImageFormField value={image} onChange={this.handleImgChange} /> */} </Grid> </Grid> </div> @@ -498,9 +606,14 @@ class AnnotationCreation extends Component { </Typography> </Grid> <Grid item xs={12}> - <Paper elevation={0} className={classes.paper}> - <ToggleButtonGroup - className={classes.grouped} + <Paper + elevation={0} + sx={{ + display: 'flex', + flexWrap: 'wrap', + }} + > + <StyledToggleButtonGroup value={activeTool} exclusive onChange={this.changeTool} @@ -513,10 +626,12 @@ class AnnotationCreation extends Component { <ToggleButton value="edit" aria-label="select cursor"> <FormatShapesIcon /> </ToggleButton> - </ToggleButtonGroup> - <Divider flexItem orientation="vertical" className={classes.divider} /> - <ToggleButtonGroup - className={classes.grouped} + </StyledToggleButtonGroup> + <StyledDivider + flexItem + orientation="vertical" + /> + <StyledToggleButtonGroup value={activeTool} exclusive onChange={this.changeTool} @@ -535,7 +650,7 @@ class AnnotationCreation extends Component { <ToggleButton value="freehand" aria-label="free hand polygon"> <GestureIcon /> </ToggleButton> - </ToggleButtonGroup> + </StyledToggleButtonGroup> </Paper> </Grid> </Grid> @@ -578,25 +693,25 @@ class AnnotationCreation extends Component { </ToggleButton> </ToggleButtonGroup> - <Divider flexItem orientation="vertical" className={classes.divider} /> + <StyledDivider flexItem orientation="vertical" /> { /* close / open polygon mode only for freehand drawing mode. */ - activeTool === 'freehand' - ? ( - <ToggleButtonGroup - size="small" - value={closedMode} - onChange={this.changeClosedMode} - > - <ToggleButton value="closed"> - <ClosedPolygonIcon /> - </ToggleButton> - <ToggleButton value="open"> - <OpenPolygonIcon /> - </ToggleButton> - </ToggleButtonGroup> - ) - : null - } + activeTool === 'freehand' + ? ( + <ToggleButtonGroup + size="small" + value={closedMode} + onChange={this.changeClosedMode} + > + <ToggleButton value="closed"> + <ClosedPolygonIcon /> + </ToggleButton> + <ToggleButton value="open"> + <OpenPolygonIcon /> + </ToggleButton> + </ToggleButtonGroup> + ) + : null + } </Grid> </Grid> </div> @@ -608,7 +723,7 @@ class AnnotationCreation extends Component { Save </Button> </div> - </form> + </StyledForm> <Popover open={lineWeightPopoverOpen} anchorEl={popoverLineWeightAnchorEl} @@ -638,7 +753,7 @@ class AnnotationCreation extends Component { onClose={this.closeChooseColor} > <SketchPicker - // eslint-disable-next-line react/destructuring-assignment + // eslint-disable-next-line react/destructuring-assignment color={this.state[currentColorType] || {}} onChangeComplete={this.updateStrokeColor} /> @@ -648,8 +763,32 @@ class AnnotationCreation extends Component { } } -/** */ -const styles = (theme) => ({ +const StyledForm = styled('form')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: '20px', + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(1), + paddingTop: theme.spacing(2), +})); + +const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({ + '&:first-child': { + borderRadius: theme.shape.borderRadius, + }, + '&:not(:first-child)': { + borderRadius: theme.shape.borderRadius, + }, + border: 'none', + margin: theme.spacing(0.5), +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: theme.spacing(1, 0.5), +})); + +/* const StyledAnnotationCreation = styled('div')(({ ownerState, theme }) => ({ buttonTimeContainer: { display: 'flex', flexDirection: 'column', @@ -668,7 +807,7 @@ const styles = (theme) => ({ margin: theme.spacing(0.5), }, MuiSliderColorSecondary: { - color: 'rgba(1, 0, 0, 0.38)', + }, paper: { display: 'flex', @@ -710,15 +849,17 @@ const styles = (theme) => ({ marginLeft: '0', marginRight: '5px', }, -}); +}); */ 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 }), + PropTypes.shape({ + id: PropTypes.string, + index: PropTypes.number, + }), ), - classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types closeCompanionWindow: PropTypes.func, config: PropTypes.shape({ annotation: PropTypes.shape({ @@ -752,4 +893,4 @@ AnnotationCreation.defaultProps = { }, }; -export default styled(styles)(AnnotationCreation); +export default AnnotationCreation; diff --git a/src/TextEditor.js b/src/TextEditor.js index 4352ff43cd4e58f3aed715d62da56d6361cf077d..e157d2bd9c458b6faa6c3e27098a1fdc31ef5aa1 100644 --- a/src/TextEditor.js +++ b/src/TextEditor.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Editor, EditorState, RichUtils } from 'draft-js'; import ToggleButton from '@mui/lab/ToggleButton'; @@ -7,50 +7,45 @@ import BoldIcon from '@mui/icons-material/FormatBold'; import ItalicIcon from '@mui/icons-material/FormatItalic'; import { stateToHTML } from 'draft-js-export-html'; import { stateFromHTML } from 'draft-js-import-html'; +import { styled } from '@mui/system'; -/** */ -class TextEditor extends Component { - /** */ - constructor(props) { - super(props); - this.state = { - editorState: EditorState.createWithContent(stateFromHTML(props.annoHtml)), - }; - 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(); - } +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), +})); - /** - * 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(); - } +function TextEditor({ annoHtml, updateAnnotationBody }) { + const [editorState, setEditorState] = useState(EditorState.createWithContent(stateFromHTML(annoHtml))); + const editorRef = useRef(null); - /** */ - handleFormating(e, newFormat) { - const { editorState } = this.state; - this.onChange(RichUtils.toggleInlineStyle(editorState, newFormat)); - } + useEffect(() => { + // Any effect that might be needed on component mount/update + }, [/* dependencies */]); - /** */ - handleKeyCommand(command, editorState) { - const newState = RichUtils.handleKeyCommand(editorState, command); + const handleFocus = () => { + editorRef.current?.focus(); + }; + + const handleFormating = (e, newFormat) => { + setEditorState(RichUtils.toggleInlineStyle(editorState, newFormat)); + }; + + const handleKeyCommand = (command, state) => { + const newState = RichUtils.handleKeyCommand(state, command); if (newState) { - this.onChange(newState); + setEditorState(newState); return 'handled'; } return 'not-handled'; - } + }; - /** */ - onChange(editorState) { - const { updateAnnotationBody } = this.props; - this.setState({ editorState }); + const onChange = (state) => { + setEditorState(state); if (updateAnnotationBody) { const options = { inlineStyles: { @@ -58,68 +53,36 @@ class TextEditor extends Component { ITALIC: { element: 'i' }, }, }; - updateAnnotationBody(stateToHTML(editorState.getCurrentContent(), options).toString()); + updateAnnotationBody(stateToHTML(state.getCurrentContent(), options).toString()); } - } + }; - /** */ - render() { - const { classes } = this.props; - const { editorState } = this.state; - const currentStyle = editorState.getCurrentInlineStyle(); + const currentStyle = editorState.getCurrentInlineStyle(); - return ( - <div> - <div className={classes.editorRoot} onClick={this.handleFocus}> - <Editor - editorState={editorState} - handleKeyCommand={this.handleKeyCommand} - onChange={this.onChange} - ref={this.editorRef} - /> - </div> - <ToggleButtonGroup - size="small" - value={currentStyle.toArray()} - > - <ToggleButton - onClick={this.handleFormating} - value="BOLD" - > - <BoldIcon/> - </ToggleButton> - <ToggleButton - onClick={this.handleFormating} - value="ITALIC" - > - <ItalicIcon/> - </ToggleButton> - </ToggleButtonGroup> - </div> - ); - } + return ( + <div> + <EditorRoot onClick={handleFocus}> + <Editor + editorState={editorState} + handleKeyCommand={handleKeyCommand} + onChange={onChange} + ref={editorRef} + /> + </EditorRoot> + <ToggleButtonGroup size="small" value={currentStyle.toArray()}> + <ToggleButton onClick={handleFormating} value="BOLD"> + <BoldIcon /> + </ToggleButton> + <ToggleButton onClick={handleFormating} value="ITALIC"> + <ItalicIcon /> + </ToggleButton> + </ToggleButtonGroup> + </div> + ); } -/** */ -const styles = (theme) => ({ - editorRoot: { - borderColor: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)', - borderRadius: theme.shape.borderRadius, - borderStyle: 'solid', - borderWidth: 1, - fontFamily: theme.typography.fontFamily, - marginBottom: theme.spacing(1), - marginTop: theme.spacing(1), - minHeight: theme.typography.fontSize * 6, - padding: theme.spacing(1), - }, -}); - TextEditor.propTypes = { annoHtml: PropTypes.string, - classes: PropTypes.shape({ - editorRoot: PropTypes.string, - }).isRequired, updateAnnotationBody: PropTypes.func, };