recogito-client-core/src/TextAnnotator.jsx

293 lines
9.0 KiB
JavaScript

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,
applyTemplate: 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();
}
}
applyTemplate = (bodies, headless) =>
this.setState({ applyTemplate: bodies, headless })
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}
applyTemplate={this.state.applyTemplate}
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}
/>
}
</>
);
}
}