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 && } { this.state.selectedRelation && } ); } }