diff --git a/src/TextAnnotator.jsx b/src/TextAnnotator.jsx
new file mode 100644
index 0000000..c71395f
--- /dev/null
+++ b/src/TextAnnotator.jsx
@@ -0,0 +1,214 @@
+import React, { Component } from 'react';
+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 = {
+ showEditor: false,
+ selectionBounds: null,
+ selectedAnnotation: null,
+
+ showRelationEditor: false,
+ selectedRelation: null
+ }
+
+ /** Helper **/
+ _clearState = () => {
+ this.setState({
+ showEditor: false,
+ selectionBounds: null,
+ selectedAnnotation: null
+ });
+ }
+
+ handleEscape = (evt) => {
+ if (evt.which === 27)
+ this.onCancelAnnotation();
+ }
+
+ componentDidMount() {
+ this.highlighter = new Highlighter(this.props.contentEl, this.props.formatter);
+
+ this.selectionHandler = new SelectionHandler(this.props.contentEl, this.highlighter);
+ 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, clientRect } = evt;
+ if (selection) {
+ this.setState({
+ showEditor: true,
+ selectionBounds: clientRect,
+ selectedAnnotation: selection
+ });
+ } else {
+ this._clearState();
+ }
+ }
+
+ /** Common handler for annotation CREATE or UPDATE **/
+ onCreateOrUpdateAnnotation = method => (annotation, previous) => {
+ // Clear the annotation layer
+ this._clearState();
+
+ this.selectionHandler.clearSelection();
+ this.highlighter.addOrUpdateAnnotation(annotation, previous);
+
+ // Call CREATE or UPDATE handler
+ this.props[method](annotation, previous);
+ }
+
+ onDeleteAnnotation = annotation => {
+ // Delete connections
+ const connections = this.relationsLayer.getConnectionsFor(annotation);
+ connections.forEach(c => c.destroy());
+
+ this._clearState();
+ this.selectionHandler.clearSelection();
+ this.highlighter.removeAnnotation(annotation);
+ }
+
+ /** Cancel button on annotation editor **/
+ onCancelAnnotation = () => {
+ this._clearState();
+ this.selectionHandler.clearSelection();
+ }
+
+ /************************/
+ /* Relation CRUD events */
+ /************************/
+
+ // Shorthand
+ closeRelationsEditor = () => {
+ this.setState({ showRelationEditor: false });
+ this.relationsLayer.resetDrawing();
+ }
+
+ /**
+ * Selection on the relations layer: open an existing
+ * or newly created connection for editing.
+ */
+ onEditRelation = relation => {
+ this.setState({
+ showRelationEditor: true,
+ selectedRelation: relation
+ });
+ }
+
+ /** 'Ok' on the relation editor popup **/
+ onCreateOrUpdateRelation = (relation, previous) => {
+ this.relationsLayer.addOrUpdateRelation(relation, previous);
+ this.closeRelationsEditor();
+ }
+
+ /** 'Delete' on the relation editor popup **/
+ onDeleteRelation = relation => {
+ this.relationsLayer.removeRelation(relation);
+ this.closeRelationsEditor();
+ }
+
+ /****************/
+ /* External API */
+ /****************/
+
+ addAnnotation = annotation => {
+ this.highlighter.addOrUpdateAnnotation(annotation);
+ }
+
+ 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.setState({ showEditor: false });
+ }
+
+ setAnnotations = annotations => {
+ this.highlighter.init(annotations);
+ this.relationsLayer.init(annotations);
+ }
+
+ getAnnotations = () => {
+ const annotations = this.highlighter.getAllAnnotations();
+ const relations = this.relationsLayer.getAllRelations();
+ return annotations.concat(relations);
+ }
+
+ setMode = mode => {
+ if (mode === 'RELATIONS') {
+ this.setState({ showEditor: false });
+
+ this.selectionHandler.enabled = false;
+
+ this.relationsLayer.readOnly = false;
+ this.relationsLayer.startDrawing();
+ } else {
+ this.setState({ showRelationEditor: false });
+
+ this.selectionHandler.enabled = true;
+
+ this.relationsLayer.readOnly = true;
+ this.relationsLayer.stopDrawing();
+ }
+ }
+
+ render() {
+ return (
+ <>
+ { this.state.showEditor &&
+
+
+ {this.props.children}
+
+
+ }
+
+ { this.state.showRelationEditor &&
+
+ }
+ >
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 7996f0c..2829134 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,4 @@
+export { default as TextAnnotator } from './TextAnnotator';
export { default as WebAnnotation } from './WebAnnotation';
export * from './editor';