Merge branch 'fcollonval-ft/delete-button' into delete-button

This commit is contained in:
Rainer Simon 2021-03-20 09:17:13 +01:00
commit 75d5c79d13
5 changed files with 317 additions and 1 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "@recogito/recogito-client-core",
"version": "1.0.2",
"version": "1.0.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

287
src/TextAnnotator.jsx Normal file
View File

@ -0,0 +1,287 @@
import React, { Component } from 'preact/compat';
import Editor from './editor/Editor';
import Highlighter from './highlighter/Highlighter';
import SelectionHandler from './selection/SelectionHandler';
import RelationsLayer from './relations/RelationsLayer';
import RelationEditor from './relations/editor/RelationEditor';
/**
* Pulls the strings between the annotation highlight layer
* and the editor popup.
*/
export default class TextAnnotator extends Component {
state = {
selectedAnnotation: null,
selectedDOMElement: null,
selectedRelation: null,
headless: false
}
/** Shorthand **/
clearState = () => {
this.setState({
selectedAnnotation: null,
selectedDOMElement: null
});
}
handleEscape = (evt) => {
if (evt.which === 27)
this.onCancelAnnotation();
}
componentDidMount() {
this.highlighter = new Highlighter(this.props.contentEl, this.props.config.formatter);
this.selectionHandler = new SelectionHandler(this.props.contentEl, this.highlighter, this.props.config.readOnly);
this.selectionHandler.on('select', this.handleSelect);
this.relationsLayer = new RelationsLayer(this.props.contentEl);
this.relationsLayer.readOnly = true; // Deactivate by default
this.relationsLayer.on('createRelation', this.onEditRelation);
this.relationsLayer.on('selectRelation', this.onEditRelation);
this.relationsLayer.on('cancelDrawing', this.closeRelationsEditor);
document.addEventListener('keydown', this.handleEscape);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleEscape);
}
/**************************/
/* Annotation CRUD events */
/**************************/
/** Selection on the text **/
handleSelect = evt => {
const { selection, element } = evt;
if (selection) {
this.setState({
selectedAnnotation: null,
selectedDOMElement: null
}, () => this.setState({
selectedAnnotation: selection,
selectedDOMElement: element
}));
if (!selection.isSelection)
this.props.onAnnotationSelected(selection.clone());
} else {
this.clearState();
}
}
/**
* A convenience method that allows the external application to
* override the autogenerated Id for an annotation.
*
* Usually, the override will happen almost immediately after
* the annotation is created. But we need to be defensive and assume
* that the override might come in with considerable delay, thus
* the user might have made further edits already.
*
* A key challenge here is that there may be dependencies between
* the original annotation and relations that were created meanwhile.
*/
overrideAnnotationId = originalAnnotation => forcedId => {
const { id } = originalAnnotation;
// After the annotation update, we need to update dependencies
// on the annotation layer, if any
const updateDependentRelations = updatedAnnotation => {
// Wait until the highlighter update has come into effect
requestAnimationFrame(() => {
this.relationsLayer.overrideTargetAnnotation(originalAnnotation, updatedAnnotation);
})
};
// Force the editors to close first, otherwise their annotations will be orphaned
if (this.state.selectedAnnotation || this.state.selectedRelation) {
this.relationsLayer.resetDrawing();
this.setState({
selectedAnnotation: null,
selectedRelation: null
}, () => {
const updated = this.highlighter.overrideId(id, forcedId);
updateDependentRelations(updated);
});
} else {
const updated = this.highlighter.overrideId(id, forcedId);
updateDependentRelations(updated);
}
}
/**
* A convenience method that allows the external application to
* override the autogenerated Id for a relation.
*
* This operation is less problematic than .overrideAnnotation().
* We just need to make sure the RelationEditor is closed, so that
* the annotation doesn't become orphaned. Otherwise, there are
* no dependencies.
*/
overrideRelationId = originalId => forcedId => {
if (this.state.selectedRelation) {
this.setState({ selectedRelation: null }, () =>
this.relationsLayer.overrideId(originalId, forcedId));
} else {
this.relationsLayer.overrideId(originalId, forcedId);
}
}
/** Common handler for annotation CREATE or UPDATE **/
onCreateOrUpdateAnnotation = method => (annotation, previous) => {
this.clearState();
this.selectionHandler.clearSelection();
this.highlighter.addOrUpdateAnnotation(annotation, previous);
// Call CREATE or UPDATE handler
if (previous)
this.props[method](annotation.clone(), previous.clone());
else
this.props[method](annotation.clone(), this.overrideAnnotationId(annotation));
}
onDeleteAnnotation = annotation => {
// Delete connections
this.relationsLayer.destroyConnectionsFor(annotation);
this.clearState();
this.selectionHandler.clearSelection();
this.highlighter.removeAnnotation(annotation);
this.props.onAnnotationDeleted(annotation);
}
/** Cancel button on annotation editor **/
onCancelAnnotation = () => {
this.clearState();
this.selectionHandler.clearSelection();
}
/************************/
/* Relation CRUD events */
/************************/
// Shorthand
closeRelationsEditor = () => {
this.setState({ selectedRelation: null });
this.relationsLayer.resetDrawing();
}
/**
* Selection on the relations layer: open an existing
* or newly created connection for editing.
*/
onEditRelation = relation => {
this.setState({
selectedRelation: relation
});
}
/** 'Ok' on the relation editor popup **/
onCreateOrUpdateRelation = (relation, previous) => {
this.relationsLayer.addOrUpdateRelation(relation, previous);
this.closeRelationsEditor();
// This method will always receive a 'previous' connection -
// if the previous is just an empty connection, fire 'create',
// otherwise, fire 'update'
const isNew = previous.annotation.bodies.length === 0;
if (isNew)
this.props.onAnnotationCreated(relation.annotation.clone(), this.overrideRelationId(relation.annotation.id));
else
this.props.onAnnotationUpdated(relation.annotation.clone(), previous.annotation.clone());
}
/** 'Delete' on the relation editor popup **/
onDeleteRelation = relation => {
this.relationsLayer.removeRelation(relation);
this.closeRelationsEditor();
this.props.onAnnotationDeleted(relation.annotation);
}
/****************/
/* External API */
/****************/
addAnnotation = annotation => {
this.highlighter.addOrUpdateAnnotation(annotation.clone());
}
removeAnnotation = annotation => {
this.highlighter.removeAnnotation(annotation);
// If the editor is currently open on this annotation, close it
const { selectedAnnotation } = this.state;
if (selectedAnnotation && annotation.isEqual(selectedAnnotation))
this.clearState();
}
setAnnotations = annotations => {
const clones = annotations.map(a => a.clone());
this.highlighter.init(clones).then(() =>
this.relationsLayer.init(clones));
}
getAnnotations = () => {
const annotations = this.highlighter.getAllAnnotations();
const relations = this.relationsLayer.getAllRelations();
return annotations.concat(relations).map(a => a.clone());
}
setMode = mode => {
if (mode === 'RELATIONS') {
this.clearState();
this.selectionHandler.enabled = false;
this.relationsLayer.readOnly = false;
this.relationsLayer.startDrawing();
} else {
this.setState({ selectedRelation: null });
this.selectionHandler.enabled = true;
this.relationsLayer.readOnly = true;
this.relationsLayer.stopDrawing();
}
}
render() {
return (
<>
{ this.state.selectedAnnotation &&
<Editor
wrapperEl={this.props.wrapperEl}
annotation={this.state.selectedAnnotation}
selectedElement={this.state.selectedDOMElement}
config={this.props.config}
env={this.props.env}
isRemovable={this.props.isRemovable}
onAnnotationCreated={this.onCreateOrUpdateAnnotation('onAnnotationCreated')}
onAnnotationUpdated={this.onCreateOrUpdateAnnotation('onAnnotationUpdated')}
onAnnotationDeleted={this.onDeleteAnnotation}
onCancel={this.onCancelAnnotation} />
}
{ this.state.selectedRelation &&
<RelationEditor
relation={this.state.selectedRelation}
onRelationCreated={this.onCreateOrUpdateRelation}
onRelationUpdated={this.onCreateOrUpdateRelation}
onRelationDeleted={this.onDeleteRelation}
onCancel={this.closeRelationsEditor}
vocabulary={this.props.relationVocabulary}
/>
}
</>
);
}
}

View File

@ -142,9 +142,18 @@ const Editor = props => {
}
};
const onRemove = () => props.onAnnotationDeleted(props.annotation);
// Use default comment + tag widget unless host app overrides
const widgets = props.config.widgets ?
props.config.widgets.map(getWidget) : DEFAULT_WIDGETS;
const removable =
currentAnnotation &&
currentAnnotation.bodies.length > 0 &&
!currentAnnotation.isSelection &&
(props.isRemovable ? props.isRemovable(currentAnnotation) : true);
return (
<div ref={element} className="r6o-editor">
<div className="r6o-arrow" />
@ -171,6 +180,12 @@ const Editor = props => {
</div>
) : (
<div className="r6o-footer">
{ removable && (
<button className="r6o-btn highlight left outline" onClick={onRemove}>
{i18n.t('Remove')}
</button>
)}
<button
className="r6o-btn outline"
onClick={onCancel}>{i18n.t('Cancel')}</button>

View File

@ -40,3 +40,14 @@
background-color:transparent;
text-shadow:none;
}
.r6o-btn.left {
margin-left: 8px;
margin-right: 0px;
float: left;
}
.r6o-btn.highlight.outline {
border:1px solid $orange;
color:$orange;
}

View File

@ -19,6 +19,9 @@ $blueish-white-hi:lighten($blueish-white, 3%);
$lightgrey-border:#e5e5e5;
$lightgrey-border-darker:#d6d7d9;
// Highlight color
$orange: rgb(230, 149, 0);
/** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **/
/** Type colors **/
/** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **/