@cogic/annotorious
Version:
A JavaScript image annotation library
288 lines (216 loc) • 8.94 kB
JSX
import React from 'react';
import ReactDOM from 'react-dom';
import Emitter from 'tiny-emitter';
import ImageAnnotator from './ImageAnnotator';
import {
Selection,
WebAnnotation,
createEnvironment,
setLocale
} from '@recogito/recogito-client-core';
import '@recogito/recogito-client-core/themes/default';
/**
* The entrypoint into the application. Provides the
* externally visible JavaScript API.
*/
export class Annotorious {
constructor(config) {
// Programmatic calls to this instance from outside are forwarded
// through a ref
this._app = React.createRef();
// Event handling via tiny-emitter
this._emitter = new Emitter();
// TODO .headless option is deprecated!
config.disableEditor = config.disableEditor || config.headless;
// Host app may supply the image as either a DOM node or ID - normalize
const imageEl = (config.image.nodeType) ?
config.image : document.getElementById(config.image);
// Wrapper div might produce unwanted margin at the bottom otherwise!
imageEl.style.display = 'block';
// Store image reference in the Environment
this._env = createEnvironment();
this._env.image = imageEl;
setLocale(config.locale, config.messages);
// We'll wrap the image in a DIV ('_element'). The application
// container DIV, which holds the editor popup, will be attached
// as a child to the same wrapper element (=a sibling to the image
// element), so that editor and image share the same offset coordinate
// space.
this._element = document.createElement('DIV');
this._element.style.position = 'relative';
this._element.style.display = 'inline-block';
imageEl.parentNode.insertBefore(this._element, imageEl);
this._element.appendChild(imageEl);
this._appContainerEl = document.createElement('DIV');
this._element.appendChild(this._appContainerEl);
// A basic ImageAnnotator with just a comment and a tag widget
ReactDOM.render(
<ImageAnnotator
ref={this._app}
env={this._env}
wrapperEl={this._element}
config={config}
onSelectionStarted={this.handleSelectionStarted}
onSelectionCreated={this.handleSelectionCreated}
onSelectionTargetChanged={this.handleSelectionTargetChanged}
onAnnotationCreated={this.handleAnnotationCreated}
onAnnotationSelected={this.handleAnnotationSelected}
onAnnotationUpdated={this.handleAnnotationUpdated}
onAnnotationDeleted={this.handleAnnotationDeleted}
onCancelSelected={this.handleCancelSelected}
onChangeSelected={this.handleChangeSelected}
onClickAnnotation={this.handleClickAnnotation}
onLoad={this.handleLoad}
onMouseEnterAnnotation={this.handleMouseEnterAnnotation}
onMouseLeaveAnnotation={this.handleMouseLeaveAnnotation} />, this._appContainerEl);
}
/********************/
/* External events */
/********************/
handleAnnotationCreated = (annotation, overrideId) =>
this._emitter.emit('createAnnotation', annotation.underlying, overrideId);
handleAnnotationDeleted = annotation =>
this._emitter.emit('deleteAnnotation', annotation.underlying);
handleAnnotationSelected = (annotation, elem) =>
this._emitter.emit('selectAnnotation', annotation.underlying, elem);
handleAnnotationUpdated = (annotation, previous) =>
this._emitter.emit('updateAnnotation', annotation.underlying, previous.underlying);
handleCancelSelected = annotation =>
this._emitter.emit('cancelSelected', annotation.underlying);
handleChangeSelected = (selected, previous) =>
this._emitter.emit('changeSelected', selected.underlying, previous.underlying);
handleClickAnnotation = (annotation, elem) =>
this._emitter.emit('clickAnnotation', annotation.underlying, elem);
handleLoad = src =>
this._emitter.emit('load', src);
handleSelectionCreated = selection =>
this._emitter.emit('createSelection', selection.underlying);
handleSelectionStarted = pt =>
this._emitter.emit('startSelection', pt);
handleSelectionTargetChanged = target =>
this._emitter.emit('changeSelectionTarget', target);
handleMouseEnterAnnotation = (annotation, elem) =>
this._emitter.emit('mouseEnterAnnotation', annotation.underlying, elem);
handleMouseLeaveAnnotation = (annotation, elem) =>
this._emitter.emit('mouseLeaveAnnotation', annotation.underlying, elem);
/******************/
/* External API */
/******************/
// Common shorthand for handling annotationOrId args
_wrap = annotationOrId =>
annotationOrId?.type === 'Annotation' ? new WebAnnotation(annotationOrId) : annotationOrId;
addAnnotation = (annotation, readOnly) =>
this._app.current.addAnnotation(new WebAnnotation(annotation, { readOnly }));
addDrawingTool = plugin =>
this._app.current.addDrawingTool(plugin);
cancelSelected = () =>
this._app.current.cancelSelected();
clearAnnotations = () =>
this.setAnnotations([]);
clearAuthInfo = () =>
this._env.user = null;
get disableEditor() {
return this._app.current.disableEditor;
}
set disableEditor(disabled) {
this._app.current.disableEditor = disabled;
}
get disableSelect() {
return this._app.current.disableSelect;
}
set disableSelect(select) {
this._app.current.disableSelect = select;
}
destroy = () => {
ReactDOM.unmountComponentAtNode(this._appContainerEl);
this._element.parentNode.insertBefore(this._env.image, this._element);
this._element.parentNode.removeChild(this._element);
}
get formatters() {
return this._app.current.formatters || [];
}
set formatters(arg) {
if (arg) {
const formatters = Array.isArray(arg) ? arg : [ arg ];
this._app.current.formatters = formatters;
} else {
this._app.current.formatters = null;
}
}
getAnnotationById = annotationId => {
const a = this._app.current.getAnnotationById(annotationId);
return a?.underlying;
}
getAnnotations = () => {
const annotations = this._app.current.getAnnotations();
return annotations.map(a => a.underlying);
}
getImageSnippetById = annotationId =>
this._app.current.getImageSnippetById(annotationId);
getSelected = () => {
const selected = this._app.current.getSelected();
return selected?.underlying;
}
getSelectedImageSnippet = () =>
this._app.current.getSelectedImageSnippet();
getCurrentDrawingTool = () => this._app.current.getCurrentDrawingTool();
getSelectedShape = () => this._app.current.getSelectedShape();
listDrawingTools = () =>
this._app.current.listDrawingTools();
loadAnnotations = url => fetch(url)
.then(response => response.json()).then(annotations => {
this.setAnnotations(annotations);
return annotations;
});
off = (event, callback) =>
this._emitter.off(event, callback);
on = (event, handler) =>
this._emitter.on(event, handler);
once = (event, handler) =>
this._emitter.once(event, handler);
get readOnly() {
return this._app.current.readOnly;
}
set readOnly(readOnly) {
this._app.current.readOnly = readOnly;
}
removeAnnotation = annotationOrId =>
this._app.current.removeAnnotation(this._wrap(annotationOrId));
removeDrawingTool = id =>
this._app.current.removeDrawingTool(id);
saveSelected = () =>
this._app.current.saveSelected();
selectAnnotation = annotationOrId => {
const selected = this._app.current.selectAnnotation(this._wrap(annotationOrId));
return selected?.underlying;
}
setAnnotations = a => {
const annotations = a || []; // Allow null arg
const webannotations = annotations.map(a => new WebAnnotation(a));
this._app.current.setAnnotations(webannotations);
}
setAuthInfo = authinfo =>
this._env.user = authinfo;
setDrawingTool = shape =>
this._app.current.setDrawingTool(shape);
setServerTime = timestamp =>
this._env.setServerTime(timestamp);
setVisible = visible =>
this._app.current.setVisible(visible);
updateSelected = (annotation, saveImmediately) => {
let updated = null;
if (annotation.type === 'Annotation')
updated = new WebAnnotation(annotation);
else if (annotation.type === 'Selection')
updated = new Selection(annotation.target, annotation.body);
if (updated)
this._app.current.updateSelected(updated, saveImmediately);
}
get widgets() {
return this._app.current.widgets;
}
set widgets(widgets) {
this._app.current.widgets = widgets;
}
}
export const init = config => new Annotorious(config);