Merge pull request #60 from recogito/draggable-editor

Draggable editor
This commit is contained in:
Rainer Simon 2021-06-19 18:14:36 +02:00 committed by GitHub
commit 730b3960cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 48 deletions

29
package-lock.json generated
View File

@ -13,6 +13,7 @@
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"node-polyglot": "^2.4.0", "node-polyglot": "^2.4.0",
"react-autosize-textarea": "^7.1.0", "react-autosize-textarea": "^7.1.0",
"react-draggable": "^4.4.3",
"react-select": "^4.3.1", "react-select": "^4.3.1",
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"timeago-react": "^3.0.2", "timeago-react": "^3.0.2",
@ -1641,6 +1642,11 @@
"fsevents": "~2.1.1" "fsevents": "~2.1.1"
} }
}, },
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@ -2956,6 +2962,15 @@
"react": "^16.14.0" "react": "^16.14.0"
} }
}, },
"node_modules/react-draggable": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz",
"integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==",
"dependencies": {
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
}
},
"node_modules/react-input-autosize": { "node_modules/react-input-autosize": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz",
@ -4903,6 +4918,11 @@
"readdirp": "~3.2.0" "readdirp": "~3.2.0"
} }
}, },
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"cliui": { "cliui": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@ -5881,6 +5901,15 @@
"scheduler": "^0.19.1" "scheduler": "^0.19.1"
} }
}, },
"react-draggable": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz",
"integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==",
"requires": {
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
}
},
"react-input-autosize": { "react-input-autosize": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz",

View File

@ -32,6 +32,7 @@
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"node-polyglot": "^2.4.0", "node-polyglot": "^2.4.0",
"react-autosize-textarea": "^7.1.0", "react-autosize-textarea": "^7.1.0",
"react-draggable": "^4.4.3",
"react-select": "^4.3.1", "react-select": "^4.3.1",
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"timeago-react": "^3.0.2", "timeago-react": "^3.0.2",

View File

@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import Draggable from 'react-draggable';
import { getWidget, DEFAULT_WIDGETS } from './widgets'; import { getWidget, DEFAULT_WIDGETS } from './widgets';
import { TrashIcon } from '../Icons'; import { TrashIcon } from '../Icons';
import setPosition from './setPosition'; import setPosition from './setPosition';
@ -22,6 +23,8 @@ const Editor = props => {
// The current state of the edited annotation vs. original // The current state of the edited annotation vs. original
const [ currentAnnotation, setCurrentAnnotation ] = useState(); const [ currentAnnotation, setCurrentAnnotation ] = useState();
const [ dragged, setDragged ] = useState(false);
// Reference to the DOM element, so we can set position // Reference to the DOM element, so we can set position
const element = useRef(); const element = useRef();
@ -46,14 +49,16 @@ const Editor = props => {
const initResizeObserver = () => { const initResizeObserver = () => {
if (window?.ResizeObserver) { if (window?.ResizeObserver) {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
setPosition(props.wrapperEl, element.current, props.selectedElement); if (!dragged)
setPosition(props.wrapperEl, element.current, props.selectedElement);
}); });
resizeObserver.observe(props.wrapperEl); resizeObserver.observe(props.wrapperEl);
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
} else { } else {
// Fire setPosition *only* for devices that don't support ResizeObserver // Fire setPosition *only* for devices that don't support ResizeObserver
setPosition(props.wrapperEl, element.current, props.selectedElement); if (!dragged)
setPosition(props.wrapperEl, element.current, props.selectedElement);
} }
} }
@ -164,51 +169,57 @@ const Editor = props => {
!widgets.some(isReadOnlyWidget); // every widget is deletable !widgets.some(isReadOnlyWidget); // every widget is deletable
return ( return (
<div ref={element} className="r6o-editor"> <Draggable
<div className="r6o-arrow" /> cancel=".r6o-btn, .r6o-nodrag"
<div className="r6o-editor-inner"> onDrag={() => setDragged(true)}>
{widgets.map(widget =>
React.cloneElement(widget, { <div ref={element} className={dragged ? 'r6o-editor dragged' : 'r6o-editor'}>
annotation : currentAnnotation, <div className="r6o-arrow" />
readOnly : props.readOnly, <div className="r6o-editor-inner">
env: props.env, {widgets.map(widget =>
onAppendBody, React.cloneElement(widget, {
onUpdateBody, annotation : currentAnnotation,
onRemoveBody, readOnly : props.readOnly,
onUpsertBody, env: props.env,
onSetProperty, onAppendBody,
onSaveAndClose: onOk onUpdateBody,
}) onRemoveBody,
)} onUpsertBody,
onSetProperty,
{ props.readOnly ? ( onSaveAndClose: onOk
<div className="r6o-footer"> })
<button )}
className="r6o-btn"
onClick={onCancel}>{i18n.t('Close')}</button> { props.readOnly ? (
</div> <div className="r6o-footer">
) : ( <button
<div className="r6o-footer"> className="r6o-btn"
{ hasDelete && ( onClick={onCancel}>{i18n.t('Close')}</button>
</div>
) : (
<div className="r6o-footer">
{ hasDelete && (
<button
className="r6o-btn left delete-annotation"
title={i18n.t('Delete')}
onClick={onDelete}>
<TrashIcon width={12} />
</button>
)}
<button <button
className="r6o-btn left delete-annotation" className="r6o-btn outline"
title={i18n.t('Delete')} onClick={onCancel}>{i18n.t('Cancel')}</button>
onClick={onDelete}>
<TrashIcon width={12} />
</button>
)}
<button <button
className="r6o-btn outline" className="r6o-btn "
onClick={onCancel}>{i18n.t('Cancel')}</button> onClick={onOk}>{i18n.t('Ok')}</button>
</div>
<button )}
className="r6o-btn " </div>
onClick={onOk}>{i18n.t('Ok')}</button>
</div>
)}
</div> </div>
</div>
</Draggable>
) )
} }

View File

@ -66,7 +66,7 @@ const Comment = props => {
/> } /> }
<div <div
className={isMenuVisible ? "r6o-icon r6o-arrow-down r6o-menu-open" : "r6o-icon r6o-arrow-down"} className={isMenuVisible ? "r6o-icon r6o-arrow-down r6o-nodrag r6o-menu-open" : "r6o-icon r6o-arrow-down r6o-nodrag"}
onClick={() => setIsMenuVisible(!isMenuVisible)}> onClick={() => setIsMenuVisible(!isMenuVisible)}>
<ChevronDownIcon width={12} /> <ChevronDownIcon width={12} />
</div> </div>

View File

@ -10,7 +10,7 @@ const DropdownMenu = props => {
useClickOutside(ref, () => props.onClickOutside()); useClickOutside(ref, () => props.onClickOutside());
return ( return (
<ul ref={ref} className="r6o-comment-dropdown-menu"> <ul ref={ref} className="r6o-comment-dropdown-menu r6o-nodrag">
<li onClick={props.onEdit}>{i18n.t('Edit')}</li> <li onClick={props.onEdit}>{i18n.t('Edit')}</li>
<li onClick={props.onDelete}>{i18n.t('Delete')}</li> <li onClick={props.onDelete}>{i18n.t('Delete')}</li>
</ul> </ul>

View File

@ -22,7 +22,7 @@ export default class TextEntryField extends Component {
return ( return (
<TextareaAutosize <TextareaAutosize
ref={this.onRender} ref={this.onRender}
className="r6o-editable-text" className={this.props.editable ? 'r6o-editable-text r6o-nodrag' : 'r6o-editable-text'}
value={this.props.content} value={this.props.content}
placeholder={this.props.placeholder || i18n.t('Add a comment...')} placeholder={this.props.placeholder || i18n.t('Add a comment...')}
disabled={!this.props.editable} disabled={!this.props.editable}

View File

@ -59,7 +59,7 @@ const TagWidget = props => {
} }
return ( return (
<div className="r6o-widget r6o-tag"> <div className="r6o-widget r6o-tag r6o-nodrag">
{ tags.length > 0 && { tags.length > 0 &&
<ul className="r6o-taglist"> <ul className="r6o-taglist">
{ tags.map(tag => { tags.map(tag =>

View File

@ -116,6 +116,10 @@
} }
.r6o-editor.dragged .r6o-arrow {
display:none;
}
.r6o-purposedropdown { .r6o-purposedropdown {
width: 150px; width: 150px;
display: inline-block; display: inline-block;