diff --git a/package.json b/package.json
index 2152b58..6683450 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@recogito/recogito-client-core",
- "version": "0.3.4",
+ "version": "1.0.0",
"description": "Core functions, classes and components for RecogitoJS",
"main": "src/index.js",
"scripts": {
diff --git a/src/TextAnnotator.jsx b/src/TextAnnotator.jsx
deleted file mode 100644
index e145196..0000000
--- a/src/TextAnnotator.jsx
+++ /dev/null
@@ -1,289 +0,0 @@
-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() {
- const readOnly = this.props.config.readOnly || this.state.selectedAnnotation?.readOnly
-
- return (
- <>
- { this.state.selectedAnnotation &&
-
- }
-
- { this.state.selectedRelation &&
-
- }
- >
- );
- }
-
-}
diff --git a/src/highlighter/Highlighter.js b/src/highlighter/Highlighter.js
deleted file mode 100644
index 9b992d1..0000000
--- a/src/highlighter/Highlighter.js
+++ /dev/null
@@ -1,301 +0,0 @@
-const TEXT = 3; // HTML DOM node type for text nodes
-
-const RENDER_BATCH_SIZE = 100; // Number of annotations to render in one frame
-
-const uniqueItems = items => Array.from(new Set(items))
-
-export default class Highlighter {
-
- constructor(element, formatter) {
- this.el = element;
- this.formatter = formatter;
- }
-
- init = annotations => new Promise((resolve, _) => {
- const startTime = performance.now();
-
- // Discard all annotations without a TextPositionSelector
- const highlights = annotations.filter(a => a.selector('TextPositionSelector'));
-
- // Sorting bottom to top significantly speeds things up,
- // because walkTextNodes will have a lot less to walk
- highlights.sort((a, b) => b.start - a.start);
-
- // Render loop
- const render = annotations => {
- const batch = annotations.slice(0, RENDER_BATCH_SIZE);
- const remainder = annotations.slice(RENDER_BATCH_SIZE);
-
- requestAnimationFrame(() => {
- batch.forEach(this._addAnnotation);
- if (remainder.length > 0) {
- render(remainder);
- } else {
- console.log(`Rendered ${highlights.length}, took ${performance.now() - startTime}ms`);
- resolve();
- }
- });
- }
-
- render(highlights);
- })
-
- _addAnnotation = annotation => {
- try {
- const [ domStart, domEnd ] = this.charOffsetsToDOMPosition([ annotation.start, annotation.end ]);
-
- const range = document.createRange();
- range.setStart(domStart.node, domStart.offset);
- range.setEnd(domEnd.node, domEnd.offset);
-
- const spans = this.wrapRange(range);
- this.applyStyles(annotation, spans);
- this.bindAnnotation(annotation, spans);
- } catch (error) {
- console.warn('Could not render annotation')
- console.warn(error);
- console.warn(annotation.underlying);
- }
- }
-
- _findAnnotationSpans = annotation => {
- const allAnnotationSpans = document.querySelectorAll('.r6o-annotation');
- return Array.prototype.slice.call(allAnnotationSpans)
- .filter(span => span.annotation.isEqual(annotation));
- }
-
- getAllAnnotations = () => {
- const allAnnotationSpans = document.querySelectorAll('.r6o-annotation');
- const allAnnotations = Array.from(allAnnotationSpans).map(span => span.annotation);
- return [...new Set(allAnnotations)];
- }
-
- addOrUpdateAnnotation = (annotation, maybePrevious) => {
- // TODO index annotation to make this faster
- const annoSpans = this._findAnnotationSpans(annotation);
- const prevSpans = maybePrevious ? this._findAnnotationSpans(maybePrevious) : [];
- const spans = uniqueItems(annoSpans.concat(prevSpans));
-
- if (spans.length > 0) {
- // naive approach
- this._unwrapHighlightings(spans);
- this.el.normalize();
- this._addAnnotation(annotation);
- } else {
- this._addAnnotation(annotation);
- }
- }
-
- removeAnnotation = annotation => {
- const spans = this._findAnnotationSpans(annotation);
- if (spans) {
- this._unwrapHighlightings(spans)
- this.el.normalize();
- }
- }
-
- /**
- * Forces a new ID on the annotation with the given ID. This method handles
- * the ID update within the Highlighter ONLY. It's up to the application to
- * keep the RelationsLayer in sync!
- *
- * @returns the updated annotation for convenience
- */
- overrideId = (originalId, forcedId) => {
- const allSpans = document.querySelectorAll(`.r6o-annotation[data-id="${originalId}"]`);
- const annotation = allSpans[0].annotation;
-
- const updatedAnnotation = annotation.clone({ id : forcedId });
- this.bindAnnotation(updatedAnnotation, allSpans);
-
- return updatedAnnotation;
- }
-
- _unwrapHighlightings(highlightSpans) {
- for (const span of highlightSpans) {
- const parent = span.parentNode;
- parent.insertBefore(document.createTextNode(span.textContent), span);
- parent.removeChild(span);
- }
- }
-
- applyStyles = (annotation, spans) => {
- const extraClasses = this.formatter ? this.formatter(annotation) : '';
- spans.forEach(span => span.className = `r6o-annotation ${extraClasses}`.trim());
- }
-
- bindAnnotation = (annotation, elements) => {
- elements.forEach(el => {
- el.annotation = annotation;
- el.dataset.id = annotation.id;
- });
- }
-
- walkTextNodes = (node, stopOffset, nodeArray) => {
- const nodes = (nodeArray) ? nodeArray : [];
-
- const offset = (function() {
- var runningOffset = 0;
- nodes.forEach(function(node) {
- runningOffset += node.textContent.length;;
- });
- return runningOffset;
- })();
-
- let keepWalking = true;
-
- if (offset > stopOffset)
- return false;
-
- if (node.nodeType === TEXT)
- nodes.push(node);
-
- node = node.firstChild;
-
- while(node && keepWalking) {
- keepWalking = this.walkTextNodes(node, stopOffset, nodes);
- node = node.nextSibling;
- }
-
- return nodes;
- }
-
- charOffsetsToDOMPosition = charOffsets => {
- const maxOffset = Math.max.apply(null, charOffsets);
-
- const textNodeProps = (() => {
- let start = 0;
- return this.walkTextNodes(this.el, maxOffset).map(function(node) {
- var nodeLength = node.textContent.length,
- nodeProps = { node: node, start: start, end: start + nodeLength };
-
- start += nodeLength;
- return nodeProps;
- });
- })();
-
- return this.calculateDomPositionWithin(textNodeProps, charOffsets);
- }
-
- /**
- * Given a rootNode, this helper gets all text between a given
- * start- and end-node. Basically combines walkTextNodes (above)
- * with a hand-coded dropWhile & takeWhile.
- */
- textNodesBetween = (startNode, endNode, rootNode) => {
- // To improve performance, don't walk the DOM longer than necessary
- var stopOffset = (function() {
- var rangeToEnd = document.createRange();
- rangeToEnd.setStart(rootNode, 0);
- rangeToEnd.setEnd(endNode, endNode.textContent.length);
- return rangeToEnd.toString().length;
- })(),
-
- allTextNodes = this.walkTextNodes(rootNode, stopOffset),
-
- nodesBetween = [],
- len = allTextNodes.length,
- take = false,
- n, i;
-
- for (i=0; i {
- var positions = [];
-
- textNodeProperties.forEach(function(props, i) {
- charOffsets.forEach(function(charOffset, j) {
- if (charOffset >= props.start && charOffset <= props.end) {
- // Don't attach nodes for the same charOffset twice
- var previousOffset = (positions.length > 0) ?
- positions[positions.length - 1].charOffset : false;
-
- if (previousOffset !== charOffset)
- positions.push({
- charOffset: charOffset,
- node: props.node,
- offset: charOffset - props.start
- });
- }
- });
-
- // Break (i.e. return false) if all positions are computed
- return positions.length < charOffsets.length;
- });
-
- return positions;
- }
-
- wrapRange = (range, commonRoot) => {
- const root = commonRoot ? commonRoot : this.el;
-
- const surround = range => {
- var wrapper = document.createElement('SPAN');
- range.surroundContents(wrapper);
- return wrapper;
- };
-
- if (range.startContainer === range.endContainer) {
- return [ surround(range) ];
- } else {
- // The tricky part - we need to break the range apart and create
- // sub-ranges for each segment
- var nodesBetween =
- this.textNodesBetween(range.startContainer, range.endContainer, root);
-
- // Start with start and end nodes
- var startRange = document.createRange();
- startRange.selectNodeContents(range.startContainer);
- startRange.setStart(range.startContainer, range.startOffset);
- var startWrapper = surround(startRange);
-
- var endRange = document.createRange();
- endRange.selectNode(range.endContainer);
- endRange.setEnd(range.endContainer, range.endOffset);
- var endWrapper = surround(endRange);
-
- // And wrap nodes in between, if any
- var centerWrappers = nodesBetween.reverse().map(function(node) {
- const wrapper = document.createElement('SPAN');
- node.parentNode.insertBefore(wrapper, node);
- wrapper.appendChild(node);
- return wrapper;
- });
-
- return [ startWrapper ].concat(centerWrappers, [ endWrapper ]);
- }
- }
-
- getAnnotationsAt = element => {
- // Helper to get all annotations in case of multipe nested annotation spans
- var getAnnotationsRecursive = function(element, a) {
- var annotations = (a) ? a : [ ],
- parent = element.parentNode;
-
- annotations.push(element.annotation);
-
- return (parent.classList.contains('r6o-annotation')) ?
- getAnnotationsRecursive(parent, annotations) : annotations;
- },
-
- sortByQuoteLength = function(annotations) {
- return annotations.sort(function(a, b) {
- return a.quote.length - b.quote.length;
- });
- };
-
- return sortByQuoteLength(getAnnotationsRecursive(element));
- }
-
-}
diff --git a/src/highlighter/index.js b/src/highlighter/index.js
deleted file mode 100644
index cb9ce60..0000000
--- a/src/highlighter/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Highlighter } from './Highlighter';
diff --git a/src/index.js b/src/index.js
index 97166d2..6cff55c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,10 +1,7 @@
export { default as Editor } from './editor/Editor';
export { default as createEnvironment } from './Environment';
export { default as I18n } from './i18n';
-export { default as TextAnnotator } from './TextAnnotator';
export { default as WebAnnotation } from './WebAnnotation';
-export * from './highlighter';
-export * from './relations';
export * from './selection';
export * from './utils';
diff --git a/src/relations/Bounds.js b/src/relations/Bounds.js
deleted file mode 100644
index 9f09d9a..0000000
--- a/src/relations/Bounds.js
+++ /dev/null
@@ -1,118 +0,0 @@
-export default class Bounds {
-
- constructor(elements, offsetContainer) {
- this.elements = elements;
- this.offsetContainer = offsetContainer;
- this.recompute();
- }
-
- recompute = () => {
- this.offsetBounds = toUnionBoundingRects(this.elements).map(clientBounds => {
- return toOffsetBounds(clientBounds, this.offsetContainer);
- });
- }
-
- get rects() {
- return this.offsetBounds;
- }
-
- get top() {
- return this.offsetBounds[0].top;
- }
-
- get bottom() {
- return this.offsetBounds[this.offsetBounds.length - 1].bottom;
- }
-
- get height() {
- return this.bottom - this.top;
- }
-
- get topHandleXY() {
- return [
- this.offsetBounds[0].left + this.offsetBounds[0].width / 2 + 0.5,
- this.offsetBounds[0].top
- ];
- }
-
- get bottomHandleXY() {
- const i = this.offsetBounds.length - 1;
- return [
- this.offsetBounds[i].left + this.offsetBounds[i].width / 2 - 0.5,
- this.offsetBounds[i].bottom
- ];
- }
-
-}
-
-/** Translates DOMRect client bounds to offset bounds within the given container **/
-const toOffsetBounds = (clientBounds, offsetContainer) => {
- const { top, left } = offsetContainer.getBoundingClientRect();
- const l = Math.round(clientBounds.left - left);
- const t = Math.round(clientBounds.top - top);
-
- return {
- left : l,
- top : t,
- right : Math.round(l + clientBounds.width),
- bottom: Math.round(t + clientBounds.height),
- width : Math.round(clientBounds.width),
- height: Math.round(clientBounds.height)
- };
-};
-
-/** Returns a clean list of (merged) DOMRect bounds for the given elements **/
-const toUnionBoundingRects = elements => {
- const allRects = elements.reduce(function(arr, el) {
- const rectList = el.getClientRects();
- const len = rectList.length;
-
- for (let i = 0; i {
- if (clientBounds.length == 1)
- return clientBounds; // shortcut
-
- return clientBounds.reduce(function(merged, bbox) {
- const previous = (merged.length > 0) ? merged[merged.length - 1] : null;
-
- const isConsecutive = function(a, b) {
- if (a.height === b.height)
- return (a.x + a.width === b.x || b.x + b.width === a.x);
- else
- return false;
- };
-
- const extend = function(a, b) {
- const { bottom, height, top } = a;
-
- const x = Math.min(a.x, b.x);
- const left = Math.min(a.left, b.left);
- const y = Math.max(a.y, b.y);
- const right = Math.max(a.right, b.right);
- const width = a.width + b.width;
-
- return { bottom, height, left, right, top, width, x, y };
- };
-
- if (previous) {
- if (isConsecutive(previous, bbox))
- merged[merged.length - 1] = extend(previous, bbox);
- else
- merged.push(bbox);
- } else {
- merged.push(bbox);
- }
-
- return merged;
- }, []);
-}
diff --git a/src/relations/Connection.js b/src/relations/Connection.js
deleted file mode 100644
index b880852..0000000
--- a/src/relations/Connection.js
+++ /dev/null
@@ -1,247 +0,0 @@
-import EventEmitter from 'tiny-emitter';
-import Bounds from './Bounds';
-import Handle from './Handle';
-import CONST from './SVGConst';
-import { getNodeById } from './RelationUtils'
-
-/**
- * The connecting line between two annotation highlights.
- */
-export default class Connection extends EventEmitter {
-
- constructor(contentEl, svgEl, nodeOrAnnotation) {
- super();
-
- this.svgEl = svgEl;
-
- // SVG elements
- this.path = document.createElementNS(CONST.NAMESPACE, 'path'),
- this.startDot = document.createElementNS(CONST.NAMESPACE, 'circle'),
- this.endDot = document.createElementNS(CONST.NAMESPACE, 'circle'),
-
- svgEl.appendChild(this.path);
- svgEl.appendChild(this.startDot);
- svgEl.appendChild(this.endDot);
-
- // Connections are initialized either from a relation annotation
- // (when loading), or as a 'floating' relation, attached to a start
- // node (when drawing a new one).
- const props = nodeOrAnnotation.type === 'Annotation' ?
- this.initFromAnnotation(contentEl, svgEl, nodeOrAnnotation) :
- this.initFromStartNode(svgEl, nodeOrAnnotation);
-
- this.annotation = props.annotation;
-
- // 'Descriptive' instance properties
- this.fromNode = props.fromNode;
- this.fromBounds = props.fromBounds;
-
- this.toNode = props.toNode;
- this.toBounds = props.toBounds;
-
- this.currentEnd = props.currentEnd;
-
- this.handle = props.handle;
-
- // A floating connection is not yet attached to an end node.
- this.floating = props.floating;
-
- this.redraw();
- }
-
- /** Initializes a fixed connection from an annotation **/
- initFromAnnotation = function(contentEl, svgEl, annotation) {
- const [ fromId, toId ] = annotation.target.map(t => t.id);
- const relation = annotation.bodies[0].value;
-
- const fromNode = getNodeById(contentEl, fromId);
- const fromBounds = new Bounds(fromNode.elements, svgEl);
-
- const toNode = getNodeById(contentEl, toId);
- const toBounds = new Bounds(toNode.elements, svgEl);
-
- const currentEnd = toNode;
-
- const handle = new Handle(relation, svgEl);
-
- // RelationsLayer uses click as a selection event
- handle.on('click', () => this.emit('click', {
- annotation,
- from: fromNode.annotation,
- to: toNode.annotation,
- midX: this.currentMidXY[0],
- midY: this.currentMidXY[1]
- }));
-
- return { annotation, fromNode, fromBounds, toNode, toBounds, currentEnd, handle, floating: false };
- }
-
- /** Initializes a floating connection from a start node **/
- initFromStartNode = function(svgEl, fromNode) {
- const fromBounds = new Bounds(fromNode.elements, svgEl);
- return { fromNode, fromBounds, floating: true };
- }
-
- /**
- * Fixes the end of the connection to the current end node,
- * turning a floating connection into a non-floating one.
- */
- unfloat = function() {
- if (this.currentEnd.elements)
- this.floating = false;
- }
-
- /** Moves the end of a (floating!) connection to the given [x,y] or node **/
- dragTo = function(xyOrNode) {
- if (this.floating) {
- this.currentEnd = xyOrNode;
- if (xyOrNode.elements) {
- this.toNode = xyOrNode;
- this.toBounds = new Bounds(xyOrNode.elements, this.svgEl);
- }
- }
- }
-
- destroy = () => {
- this.svgEl.removeChild(this.path);
- this.svgEl.removeChild(this.startDot);
- this.svgEl.removeChild(this.endDot);
-
- if (this.handle)
- this.handle.destroy();
- }
-
- /** Redraws this connection **/
- redraw = function() {
- if (this.currentEnd) {
- const end = this.endXY;
-
- const startsAtTop = end[1] <= (this.fromBounds.top + this.fromBounds.height / 2);
- const start = (startsAtTop) ?
- this.fromBounds.topHandleXY : this.fromBounds.bottomHandleXY;
-
- const deltaX = end[0] - start[0];
- const deltaY = end[1] - start[1];
-
- const half = (Math.abs(deltaX) + Math.abs(deltaY)) / 2; // Half of length, for middot pos computation
- const midX = (half > Math.abs(deltaX)) ? start[0] + deltaX : start[0] + half * Math.sign(deltaX);
-
- let midY; // computed later
-
- const orientation = (half > Math.abs(deltaX)) ?
- (deltaY > 0) ? 'down' : 'up' :
- (deltaX > 0) ? 'right' : 'left';
-
- const d = CONST.LINE_DISTANCE - CONST.BORDER_RADIUS; // Shorthand: vertical straight line length
-
- // Path that starts at the top edge of the annotation highlight
- const compileBottomPath = function() {
- const arc1 = (deltaX > 0) ? CONST.ARC_9CC : CONST.ARC_3CW;
- const arc2 = (deltaX > 0) ? CONST.ARC_0CW : CONST.ARC_0CC;
-
- midY = (half > Math.abs(deltaX)) ?
- start[1] + half - Math.abs(deltaX) + CONST.LINE_DISTANCE :
- start[1] + CONST.LINE_DISTANCE;
-
- return 'M' + start[0] +
- ' ' + start[1] +
- 'v' + d +
- arc1 +
- 'h' + (deltaX - 2 * Math.sign(deltaX) * CONST.BORDER_RADIUS) +
- arc2 +
- 'V' + end[1];
- };
-
- // Path that starts at the bottom edge of the annotation highlight
- const compileTopPath = function() {
- const arc1 = (deltaX > 0) ? CONST.ARC_9CW : CONST.ARC_3CC;
- const arc2 = (deltaX > 0) ?
- (deltaY >= 0) ? CONST.ARC_0CW : CONST.ARC_6CC :
- (deltaY >= 0) ? CONST.ARC_0CC : CONST.ARC_6CW;
-
- midY = (half > Math.abs(deltaX)) ?
- start[1] - (half - Math.abs(deltaX)) - CONST.LINE_DISTANCE :
- start[1] - CONST.LINE_DISTANCE;
-
- return 'M' + start[0] +
- ' ' + start[1] +
- 'v-' + (CONST.LINE_DISTANCE - CONST.BORDER_RADIUS) +
- arc1 +
- 'h' + (deltaX - 2 * Math.sign(deltaX) * CONST.BORDER_RADIUS) +
- arc2 +
- 'V' + end[1];
- };
-
- this.startDot.setAttribute('cx', start[0]);
- this.startDot.setAttribute('cy', start[1]);
- this.startDot.setAttribute('r', 2);
- this.startDot.setAttribute('class', 'start');
-
- this.endDot.setAttribute('cx', end[0]);
- this.endDot.setAttribute('cy', end[1]);
- this.endDot.setAttribute('r', 2);
- this.endDot.setAttribute('class', 'end');
-
- if (startsAtTop)
- this.path.setAttribute('d', compileTopPath());
- else
- this.path.setAttribute('d', compileBottomPath());
-
- this.path.setAttribute('class', 'connection');
-
- this.currentMidXY = [ midX, midY ];
-
- if (this.handle)
- this.handle.setPosition(this.currentMidXY, orientation);
- }
- }
-
- /**
- * Redraws this connection, and additionally forces a recompute of
- * the start and end coordinates. This is only needed if the position
- * of the annotation highlights changes, e.g. after a window resize.
- */
- recompute = () => {
- this.fromBounds.recompute();
-
- if (this.currentEnd.elements)
- this.toBounds.recompute();
-
- this.redraw();
- }
-
- /**
- * Returns true if the given relation matches this connection,
- * meaning that this connection has the same start and end point
- * as recorded in the relation.
- */
- matchesRelation = relation =>
- relation.from.isEqual(this.fromNode.annotation) &&
- relation.to.isEqual(this.toNode.annotation);
-
- /** Getter/setter shorthands **/
-
- get isFloating() {
- return this.floating;
- }
-
- get startAnnotation() {
- return this.fromNode.annotation;
- }
-
- get endAnnotation() {
- return this.toNode.annotation;
- }
-
- get endXY() {
- return (this.currentEnd instanceof Array) ?
- this.currentEnd :
- (this.fromBounds.top > this.toBounds.top) ?
- this.toBounds.bottomHandleXY : this.toBounds.topHandleXY;
- }
-
- get midXY() {
- return this.currentMidXY;
- }
-
-}
diff --git a/src/relations/Handle.js b/src/relations/Handle.js
deleted file mode 100644
index 41386df..0000000
--- a/src/relations/Handle.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import EventEmitter from 'tiny-emitter';
-import CONST from './SVGConst';
-
-const escapeHtml = unsafe => {
- return unsafe
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
-export default class Handle extends EventEmitter {
-
- constructor(label, svg) {
- super();
-
- this.svg = svg;
-
- this.g = document.createElementNS(CONST.NAMESPACE, 'g');
- this.text = document.createElementNS(CONST.NAMESPACE, 'text');
- this.rect = document.createElementNS(CONST.NAMESPACE, 'rect');
- this.arrow = document.createElementNS(CONST.NAMESPACE, 'path');
-
- // Append first and init afterwards, so we can query text width/height
- this.g.appendChild(this.rect);
- this.g.appendChild(this.text);
- this.g.appendChild(this.arrow);
- this.svg.appendChild(this.g);
-
- this.g.setAttribute('class', 'handle');
-
- this.text.innerHTML = escapeHtml(label);
-
- this.bounds = this.text.getBBox();
-
- this.text.setAttribute('dy', 2);
- this.text.setAttribute('dx', - Math.round(this.bounds.width / 2));
-
- this.rect.setAttribute('rx', 2); // Rounded corners
- this.rect.setAttribute('ry', 2);
- this.rect.setAttribute('width', Math.round(this.bounds.width) + 5);
- this.rect.setAttribute('height', Math.round(this.bounds.height));
-
- this.arrow.setAttribute('class', 'r6o-arrow');
-
- this.rect.addEventListener('click', () => this.emit('click'));
- }
-
- setPosition = (xy, orientation) => {
- const x = Math.round(xy[0]) - 0.5;
- const y = Math.round(xy[1]);
-
- const dx = Math.round(this.bounds.width / 2);
-
- const createArrow = function() {
- if (orientation === 'left')
- return 'M' + (xy[0] - dx - 8) + ',' + (xy[1] - 4) + 'l-7,4l7,4';
- else if (orientation === 'right')
- return 'M' + (xy[0] + dx + 8) + ',' + (xy[1] - 4) + 'l7,4l-7,4';
- else if (orientation === 'down')
- return 'M' + (xy[0] - 4) + ',' + (xy[1] + 12) + 'l4,7l4,-7';
- else
- return 'M' + (xy[0] - 4) + ',' + (xy[1] - 12) + 'l4,-7l4,7';
- };
-
- this.rect.setAttribute('x', x - 3 - dx);
- this.rect.setAttribute('y', y - 6.5);
-
- this.text.setAttribute('x', x);
- this.text.setAttribute('y', y);
-
- this.arrow.setAttribute('d', createArrow());
- }
-
- destroy = () =>
- this.svg.removeChild(this.g);
-
-};
\ No newline at end of file
diff --git a/src/relations/RelationUtils.js b/src/relations/RelationUtils.js
deleted file mode 100644
index d439f3b..0000000
--- a/src/relations/RelationUtils.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Returns the 'graph node' ({ annotation: ..., elements: ...}) for the
- * given annotation ID.
- */
-export const getNodeById = function(contentEl, annotationId) {
- const elements = contentEl.querySelectorAll(`*[data-id="${annotationId}"]`);
- return (elements.length > 0) ?
- { annotation: elements[0].annotation, elements: Array.from(elements) } : null;
-};
-
-/**
- * Returns the graph node for the target of the given mouse event (or
- * null if the event target is not an annotation span).
- */
-export const getNodeForEvent = function(evt) {
- // Sorts annotations by length, so we can reliably get the inner-most
- const sortByQuoteLengthDesc = annotations =>
- annotations.sort((a, b) => a.quote.length - b.quote.length);
-
- const annotationSpan = evt.target.closest('.r6o-annotation');
-
- if (annotationSpan) {
- // All stacked annotation spans
- const spans = getAnnotationSpansRecursive(annotationSpan);
-
- // Annotation from the inner-most span in the stack
- const annotation = sortByQuoteLengthDesc(spans.map(span => span.annotation))[0];
-
- // ALL spans for this annotation (not just the hovered one)
- const elements = document.querySelectorAll(`.r6o-annotation[data-id="${annotation.id}"]`);
-
- return { annotation, elements: Array.from(elements) };
- }
-}
-
-/**
- * Helper: gets all stacked annotation SPANS for an element.
- *
- * Reminder - annotations can be nested. This helper retrieves the whole stack.
- *
- *
- *
- * foo
- *
- *
- */
-export const getAnnotationSpansRecursive = function(element, s) {
- const spans = s ? s : [ ];
- spans.push(element);
-
- const parent = element.parentNode;
-
- return parent.classList.contains('r6o-annotation') ?
- getAnnotationSpansRecursive(parent, spans) : spans;
-}
\ No newline at end of file
diff --git a/src/relations/RelationsLayer.js b/src/relations/RelationsLayer.js
deleted file mode 100644
index 5b9a135..0000000
--- a/src/relations/RelationsLayer.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import Connection from './Connection';
-import DrawingTool from './drawing/DrawingTool';
-import CONST from './SVGConst';
-import EventEmitter from 'tiny-emitter';
-
-import './RelationsLayer.scss';
-
-export default class RelationsLayer extends EventEmitter {
-
- constructor(contentEl) {
- super();
-
- this.connections = [];
-
- this.contentEl = contentEl;
-
- this.svg = document.createElementNS(CONST.NAMESPACE, 'svg');
- this.svg.setAttribute('class', 'r6o-relations-layer');
- this.contentEl.appendChild(this.svg);
-
- this.drawingTool = new DrawingTool(contentEl, this.svg);
-
- // Forward events
- this.drawingTool.on('createRelation', relation => this.emit('createRelation', relation));
- this.drawingTool.on('cancelDrawing', () => this.emit('cancelDrawing'));
-
- // Redraw on window resize
- window.addEventListener('resize', () => requestAnimationFrame(() => {
- this.recomputeAll();
- }));
- }
-
- /** Shorthand **/
- createConnection = annotation => {
- const c = new Connection(this.contentEl, this.svg, annotation);
-
- // Forward click event as selection, unless we're read-only
- c.on('click', relation => this.emit('selectRelation', relation));
-
- return c
- }
-
- init = annotations => {
- // Filter annotations for 'relationship annotation' shape first
- this.connections = annotations.filter(annotation => {
- const allTargetsHashIDs = annotation.targets.every(t => t.id && t.id.startsWith('#'))
- return allTargetsHashIDs && annotation.motivation === 'linking';
- }).reduce((conns, annotation) => {
- try {
- const c = this.createConnection(annotation);
- return [ ...conns, c ];
- } catch (error) {
- console.log(error);
- console.log(`Error rendering relation for annotation ${annotation.id}`);
- return conns;
- }
- }, [])
- }
-
- recomputeAll = () => {
- this.connections.forEach(conn => {
- conn.recompute();
- });
- }
-
- addOrUpdateRelation = (relation, maybePrevious) => {
- const previous = maybePrevious ?
- this.connections.find(c => c.matchesRelation(relation)) : null;
-
- if (previous) {
- // Replace existing
- this.connections = this.connections.map(connection => {
- if (connection == previous) {
- connection.destroy();
- return this.createConnection(relation.annotation);
- } else {
- return connection;
- }
- });
- } else {
- // Add new
- const c = this.createConnection(relation.annotation);
- this.connections.push(c);
- }
- }
-
- removeRelation = relation => {
- const toRemove = this.connections.find(c => c.matchesRelation(relation));
-
- if (toRemove) {
- this.connections = this.connections.filter(c => c !== toRemove);
- toRemove.destroy();
- }
- }
-
- /** Overrides the ID for an existing relation **/
- overrideRelationId = (originalId, forcedId) => {
- const conn = this.connections.find(c => c.annotation.id == originalId);
- const updatedAnnotation = conn.annotation.clone({ id : forcedId });
- conn.annotation = updatedAnnotation;
- return conn;
- }
-
- /** Overrides the given source or target annotation **/
- overrideTargetAnnotation = (originalAnnotation, forcedAnnotation) => {
- const affectedFrom = this.connections.filter(c => c.fromNode.annotation == originalAnnotation);
- affectedFrom.forEach(c => c.fromNode.annotation = forcedAnnotation);
-
- const affectedTo = this.connections.filter(c => c.toNode.annotation == originalAnnotation);
- affectedTo.forEach(c => c.toNode.annotation = forcedAnnotation);
- }
-
- getAllRelations = () => {
- return this.connections.map(c => c.annotation);
- }
-
- /**
- * Get the relations that have the given annotation as start
- * or end node.
- */
- getConnectionsFor = annotation => {
- return this.connections.filter(c =>
- c.startAnnotation.isEqual(annotation) || c.endAnnotation.isEqual(annotation));
- }
-
- destroyConnectionsFor = annotation => {
- const connections = this.getConnectionsFor(annotation);
- connections.forEach(c => c.destroy());
- this.connections = this.connections.filter(c => !connections.includes(c));
- }
-
- show = () =>
- this.svg.style.display = 'inital';
-
- hide = () => {
- this.drawingEnabled = false;
- this.svg.style.display = 'none';
- }
-
- startDrawing = () =>
- this.drawingTool.enabled = true;
-
- stopDrawing = () =>
- this.drawingTool.enabled = false;
-
- resetDrawing = () =>
- this.drawingTool.reset();
-
- get readOnly() {
- this.svg.classList.contains('readonly');
- }
-
- set readOnly(readOnly) {
- if (readOnly)
- this.svg.setAttribute('class', 'r6o-relations-layer readonly');
- else
- this.svg.setAttribute('class', 'r6o-relations-layer');
- }
-
-}
diff --git a/src/relations/RelationsLayer.scss b/src/relations/RelationsLayer.scss
deleted file mode 100644
index 7cd2723..0000000
--- a/src/relations/RelationsLayer.scss
+++ /dev/null
@@ -1,69 +0,0 @@
-// Standard 'black' (well, not quite) type
-$line-color:#3f3f3f;
-
-.r6o-drawing {
- cursor:none;
-}
-
-.r6o-relations-layer.readonly {
-
- .handle rect {
- pointer-events:none;
- }
-
-}
-
-.r6o-relations-layer {
- position:absolute;
- top:0;
- left:0;
- width:100%;
- height:100%;
- pointer-events:none;
-
- circle {
- stroke:lighten($line-color, 7%);
- stroke-width:0.4;
- fill:$line-color;
- }
-
- path {
- stroke:lighten($line-color, 10%);
- stroke-linecap:round;
- stroke-linejoin:round;
- fill:transparent;
- }
-
- path.connection {
- stroke-width:1.6;
- stroke-dasharray:2,3;
- }
-
- path.r6o-arrow {
- stroke-width:1.8;
- fill:lighten($line-color, 25%);
- }
-
- .handle {
-
- rect {
- stroke-width:1;
- stroke:lighten($line-color, 10%);
- fill:#fff;
- pointer-events:auto;
- cursor:pointer;
- }
-
- text {
- font-size:10px;
- }
-
- }
-
- .hover {
- stroke:rgba($line-color, 0.9);
- stroke-width:1.4;
- fill:transparent;
- }
-
-}
\ No newline at end of file
diff --git a/src/relations/SVGConst.js b/src/relations/SVGConst.js
deleted file mode 100644
index 5949a63..0000000
--- a/src/relations/SVGConst.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Rounded path corner radius
-const BORDER_RADIUS = 3;
-
-// Horizontal distance between connection line and annotation highlight
-const LINE_DISTANCE = 6.5;
-
-const SVGConst = {
-
- NAMESPACE : 'http://www.w3.org/2000/svg',
-
- // Rounded corner arc radius
- BORDER_RADIUS : BORDER_RADIUS,
-
- // Horizontal distance between connection line and annotation highlight
- LINE_DISTANCE : LINE_DISTANCE,
-
- // Possible rounded corner SVG arc configurations: clock position + clockwise/counterclockwise
- ARC_0CW : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 1 ' + BORDER_RADIUS + ',' + BORDER_RADIUS,
- ARC_0CC : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 0 -' + BORDER_RADIUS + ',' + BORDER_RADIUS,
- ARC_3CW : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 1 -' + BORDER_RADIUS + ',' + BORDER_RADIUS,
- ARC_3CC : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 0 -' + BORDER_RADIUS + ',-' + BORDER_RADIUS,
- ARC_6CW : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 1 -' + BORDER_RADIUS + ',-' + BORDER_RADIUS,
- ARC_6CC : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 0 ' + BORDER_RADIUS + ',-' + BORDER_RADIUS,
- ARC_9CW : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 1 ' + BORDER_RADIUS + ',-' + BORDER_RADIUS,
- ARC_9CC : 'a' + BORDER_RADIUS + ',' + BORDER_RADIUS + ' 0 0 0 ' + BORDER_RADIUS + ',' + BORDER_RADIUS
-
-}
-
-export default SVGConst;
\ No newline at end of file
diff --git a/src/relations/drawing/DrawingTool.js b/src/relations/drawing/DrawingTool.js
deleted file mode 100644
index f914682..0000000
--- a/src/relations/drawing/DrawingTool.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import EventEmitter from 'tiny-emitter';
-import Connection from '../Connection';
-import HoverEmphasis from './HoverEmphasis';
-import { getNodeForEvent } from '../RelationUtils';
-import WebAnnotation from '../../WebAnnotation';
-
-/**
- * Wraps an event handler for event delegation. This way, we
- * can listen to events emitted by children matching the given
- * selector, rather than attaching (loads of!) handlers to each
- * child individually.
- */
-const delegatingHandler = (selector, handler) => evt => {
- if (evt.target.matches(selector))
- handler(evt);
-}
-
-/**
- * The drawing tool for creating a new relation.
- */
-export default class DrawingTool extends EventEmitter {
-
- constructor(contentEl, svgEl) {
- super();
-
- this.contentEl = contentEl;
- this.svgEl = svgEl;
-
- this.currentHover = null;
- this.currentConnection = null;
- }
-
- attachHandlers = () => {
- this.contentEl.classList.add('r6o-noselect');
-
- this.contentEl.addEventListener('mousedown', this.onMouseDown);
- this.contentEl.addEventListener('mousemove', this.onMouseMove);
- this.contentEl.addEventListener('mouseup', this.onMouseUp);
-
- this.contentEl.addEventListener('mouseover', this.onEnterAnnotation);
- this.contentEl.addEventListener('mouseout', this.onLeaveAnnotation);
-
- document.addEventListener('keydown', this.onKeyDown);
- }
-
- detachHandlers = () => {
- this.contentEl.classList.remove('r6o-noselect');
-
- this.contentEl.removeEventListener('mousedown', this.onMouseDown);
- this.contentEl.removeEventListener('mousemove', this.onMouseMove);
- this.contentEl.removeEventListener('mouseup', this.onMouseUp);
-
- this.contentEl.removeEventListener('mouseover', this.onEnterAnnotation);
- this.contentEl.removeEventListener('mouseleave', this.onLeaveAnnotation);
-
- document.removeEventListener('keydown', this.onKeyDown);
- }
-
- onMouseDown = evt => {
- const node = getNodeForEvent(evt);
- if (node) {
- if (this.currentConnection) {
- this.completeConnection(node);
- } else {
- this.startNewConnection(node);
- }
- }
- }
-
- onMouseMove = evt => {
- if (this.currentConnection && this.currentConnection.isFloating) {
- if (this.currentHover) {
- this.currentConnection.dragTo(this.currentHover.node);
- } else {
- const { top, left } = this.contentEl.getBoundingClientRect();
- this.currentConnection.dragTo([ evt.pageX - left, evt.pageY - top ]);
- }
- }
- }
-
- /**
- * We want to support both possible drawing modes: click once for start
- * and once for end; or click and hold to start, drag to end and release.
- */
- onMouseUp = () => {
- if (this.currentHover && this.currentConnection && this.currentConnection.isFloating) {
- // If this is a different node than the start node, complete the connection
- if (this.currentHover.annotation !== this.currentConnection.startAnnotation) {
- this.completeConnection(this.currentHover.node);
- }
- }
- }
-
- onKeyDown = evt => {
- if (evt.which === 27) { // Escape
- this.reset();
- this.emit('cancelDrawing');
- }
- }
-
- /** Emphasise hovered annotation **/
- onEnterAnnotation = delegatingHandler('.r6o-annotation', evt => {
- if (this.currentHover)
- this.hover();
-
- this.hover(getNodeForEvent(evt).elements);
- });
-
- /** Clear hover emphasis **/
- onLeaveAnnotation = delegatingHandler('.r6o-annotation', evt => {
- this.hover();
- });
-
- /** Drawing code for hover emphasis */
- hover = elements => {
- if (elements) {
- this.currentHover = new HoverEmphasis(this.svgEl, elements);
- } else { // Clear hover
- if (this.currentHover)
- this.currentHover.destroy();
-
- this.currentHover = null;
- }
- }
-
- /** Start drawing a new connection line **/
- startNewConnection = fromNode => {
- this.currentConnection = new Connection(this.contentEl, this.svgEl, fromNode);
- this.contentEl.classList.add('r6o-drawing');
- this.render();
- }
-
- /** Complete drawing of a new connection **/
- completeConnection = function() {
- this.currentConnection.unfloat();
-
- this.contentEl.classList.remove('r6o-drawing');
-
- const from = this.currentConnection.startAnnotation;
- const to = this.currentConnection.endAnnotation;
- const [ midX, midY ] = this.currentConnection.midXY;
-
- const annotation = WebAnnotation.create({
- target: [
- { id: from.id },
- { id: to.id }
- ]
- });
-
- this.emit('createRelation', { annotation, from, to, midX, midY });
- }
-
- reset = () => {
- if (this.currentConnection) {
- this.currentConnection.destroy();
- this.currentConnection = null;
- this.contentEl.classList.remove('r6o-drawing');
- }
- }
-
- render = () => {
- if (this.currentConnection) {
- this.currentConnection.redraw();
- requestAnimationFrame(this.render);
- }
- }
-
- set enabled(enabled) {
- if (enabled) {
- this.attachHandlers();
- } else {
- this.detachHandlers();
- this.contentEl.classList.remove('r6o-drawing');
-
- if (this.currentConnection) {
- this.currentConnection.destroy();
- this.currentConnection = null;
- }
- }
- }
-
-}
diff --git a/src/relations/drawing/HoverEmphasis.js b/src/relations/drawing/HoverEmphasis.js
deleted file mode 100644
index 015b9e1..0000000
--- a/src/relations/drawing/HoverEmphasis.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Bounds from '../Bounds';
-import CONST from '../SVGConst';
-
-export default class HoverEmphasis {
-
- constructor(svgEl, elements) {
- this.annotation = elements[0].annotation;
- this.node = { annotation: this.annotation, elements };
-
- this.outlines = document.createElementNS(CONST.NAMESPACE, 'g');
-
- const bounds = new Bounds(elements, svgEl);
-
- bounds.rects.forEach(r => {
- const rect = document.createElementNS(CONST.NAMESPACE, 'rect');
-
- rect.setAttribute('x', r.left - 0.5);
- rect.setAttribute('y', r.top - 0.5);
- rect.setAttribute('width', r.width + 1);
- rect.setAttribute('height', r.height);
- rect.setAttribute('class', 'hover');
-
- this.outlines.appendChild(rect);
- });
-
- svgEl.appendChild(this.outlines);
-
- this.svgEl = svgEl;
- }
-
- destroy = () => {
- this.svgEl.removeChild(this.outlines);
- }
-
-}
\ No newline at end of file
diff --git a/src/relations/editor/RelationEditor.jsx b/src/relations/editor/RelationEditor.jsx
deleted file mode 100644
index b80491b..0000000
--- a/src/relations/editor/RelationEditor.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { Component } from 'preact/compat';
-import { TrashIcon, CheckIcon } from '../../Icons';
-import Autocomplete from '../../editor/widgets/Autocomplete';
-
-/**
- * Shorthand to get the label (= first tag body value) from the
- * annotation of a relation.
- */
-const getContent = relation => {
- const firstTag = relation.annotation.bodies.find(b => b.purpose === 'tagging');
- return firstTag ? firstTag.value : '';
-}
-
-/**
- * A React component for the relationship editor popup. Note that this
- * component is NOT wired into the RelationsLayer directly, but needs
- * to be used separately by the implementing application. We
- * still keep it in the /recogito-relations folder though, so that
- * all code that belongs together stays together.
- */
-export default class RelationEditor extends Component {
-
- constructor(props) {
- super(props);
-
- this.element = React.createRef();
- }
-
- componentDidMount() {
- this.setPosition();
- }
-
- componentDidUpdate() {
- this.setPosition();
- }
-
- setPosition() {
- if (this.element.current) {
- const el = this.element.current;
- const { midX, midY } = this.props.relation;
-
- el.style.top = `${midY}px`;
- el.style.left = `${midX}px`;
- }
- }
-
- onSubmit = () => {
- const value = this.element.current.querySelector('input').value;
-
- const updatedAnnotation = this.props.relation.annotation.clone({
- motivation: 'linking',
- body: [{
- type: 'TextualBody',
- value,
- purpose: 'tagging'
- }]
- });
-
- const updatedRelation = { ...this.props.relation, annotation: updatedAnnotation };
-
- if (value) {
- // Fire create or update event
- if (this.props.relation.annotation.bodies.length === 0)
- this.props.onRelationCreated(updatedRelation, this.props.relation);
- else
- this.props.onRelationUpdated(updatedRelation, this.props.relation);
- } else {
- // Leaving the tag empty and hitting Enter equals cancel
- this.props.onCancel();
- }
- }
-
- onDelete = () =>
- this.props.onRelationDeleted(this.props.relation);
-
- render() {
- return(
-
- )
- }
-
-}
\ No newline at end of file
diff --git a/src/relations/index.js b/src/relations/index.js
deleted file mode 100644
index 0e48a96..0000000
--- a/src/relations/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as RelationsLayer } from './RelationsLayer';
-export { default as RelationEditor } from './editor/RelationEditor';
diff --git a/src/selection/SelectionHandler.js b/src/selection/SelectionHandler.js
deleted file mode 100644
index 69dd9a8..0000000
--- a/src/selection/SelectionHandler.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { trimRange, rangeToSelection, enableTouch, getExactOverlaps } from './SelectionUtils';
-import EventEmitter from 'tiny-emitter';
-
-const IS_TOUCH = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
-
-const IS_INTERNET_EXPLORER =
- navigator?.userAgent.match(/(MSIE|Trident)/);
-
-/** Tests whether maybeChildEl is contained in containerEl **/
-const contains = (containerEl, maybeChildEl) => {
- if (IS_INTERNET_EXPLORER) {
- // In IE, .contains returns false for text nodes
- // https://stackoverflow.com/questions/44140712/ie-acting-strange-with-node-contains-and-text-nodes
- if (maybeChildEl.nodeType == Node.TEXT_NODE)
- return containerEl === maybeChildEl.parentNode || containerEl.contains(maybeChildEl.parentNode);
- else
- return containerEl.contains(maybeChildEl);
- } else {
- // Things can be so simple, unless you're in IE
- return containerEl.contains(maybeChildEl);
- }
-}
-
-export default class SelectionHandler extends EventEmitter {
-
- constructor(element, highlighter, readOnly) {
- super();
-
- this.el = element;
- this.highlighter = highlighter;
- this.readOnly = readOnly;
-
- this.isEnabled = true;
-
- element.addEventListener('mousedown', this._onMouseDown);
- element.addEventListener('mouseup', this._onMouseUp);
-
- if (IS_TOUCH)
- enableTouch(element, this._onMouseUp);
- }
-
- get enabled() {
- return this.isEnabled;
- }
-
- set enabled(enabled) {
- this.isEnabled = enabled;
- }
-
- _onMouseDown = evt => {
- this.clearSelection();
- }
-
- _onMouseUp = evt => {
- if (this.isEnabled) {
- const selection = getSelection();
-
- if (selection.isCollapsed) {
- const annotationSpan = evt.target.closest('.r6o-annotation');
- if (annotationSpan) {
- this.emit('select', {
- selection: this.highlighter.getAnnotationsAt(annotationSpan)[0],
- element: annotationSpan
- });
- } else {
- // De-select
- this.emit('select', {});
- }
- } else if (!this.readOnly) {
- const selectedRange = trimRange(selection.getRangeAt(0));
-
- // Make sure the selection is entirely inside this.el
- const { commonAncestorContainer } = selectedRange;
-
- if (contains(this.el, commonAncestorContainer)) {
- const stub = rangeToSelection(selectedRange, this.el);
-
- const spans = this.highlighter.wrapRange(selectedRange);
- spans.forEach(span => span.className = 'r6o-selection');
-
- this._hideNativeSelection();
-
- const exactOverlaps = getExactOverlaps(stub, spans);
- if (exactOverlaps.length > 0) {
- // User selected existing - reuse top-most original to avoid stratification
- this.clearSelection();
- this.emit('select', {
- selection: exactOverlaps[0],
- element: evt.target.closest('.r6o-annotation')
- });
- } else {
- this.emit('select', {
- selection: stub,
- element: selectedRange
- });
- }
- }
- }
- }
- }
-
- _hideNativeSelection = () => {
- this.el.classList.add('r6o-hide-selection');
- }
-
- clearSelection = () => {
- this._currentSelection = null;
-
- // Remove native selection, if any
- if (window.getSelection) {
- if (window.getSelection().empty) { // Chrome
- window.getSelection().empty();
- } else if (window.getSelection().removeAllRanges) { // Firefox
- window.getSelection().removeAllRanges();
- }
- } else if (document.selection) { // IE?
- document.selection.empty();
- }
-
- this.el.classList.remove('r6o-hide-selection');
-
- const spans = Array.prototype.slice.call(this.el.querySelectorAll('.r6o-selection'));
- if (spans) {
- spans.forEach(span => {
- const parent = span.parentNode;
- parent.insertBefore(document.createTextNode(span.textContent), span);
- parent.removeChild(span);
- });
- }
- this.el.normalize();
- }
-
-}
diff --git a/src/selection/SelectionUtils.js b/src/selection/SelectionUtils.js
deleted file mode 100644
index 2f30ec0..0000000
--- a/src/selection/SelectionUtils.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import Selection from './Selection';
-
-export const trimRange = range => {
- let quote = range.toString();
- let leadingSpaces = 0;
- let trailingSpaces = 0;
-
- // Count/strip leading spaces
- while (quote.substring(0, 1) === ' ') {
- leadingSpaces += 1;
- quote = quote.substring(1);
- }
-
- // Count/strip trailing spaces
- while (quote.substring(quote.length - 1) === ' ') {
- trailingSpaces += 1;
- quote = quote.substring(0, quote.length - 1);
- }
-
- // Adjust range
- if (leadingSpaces > 0)
- range.setStart(range.startContainer, range.startOffset + leadingSpaces);
-
- if (trailingSpaces > 0)
- range.setEnd(range.endContainer, range.endOffset - trailingSpaces);
-
- return range;
-};
-
-export const rangeToSelection = (range, containerEl) => {
- const rangeBefore = document.createRange();
-
- // A helper range from the start of the contentNode to the start of the selection
- rangeBefore.setStart(containerEl, 0);
- rangeBefore.setEnd(range.startContainer, range.startOffset);
-
- const quote = range.toString();
- const start = rangeBefore.toString().length;
-
- return new Selection({
- selector: [{
- type: 'TextQuoteSelector',
- exact: quote
- }, {
- type: 'TextPositionSelector',
- start: start,
- end: start + quote.length
- }]
- });
-
-};
-
-/**
- * Util function that checks if the given selection is an exact overlap to any
- * existing annotations, and returns them, if so
- */
-export const getExactOverlaps = (newAnnotation, selectedSpans) => {
- // All existing annotations at this point
- const existingAnnotations = [];
-
- selectedSpans.forEach(span => {
- const enclosingAnnotationSpan = span.closest('.r6o-annotation');
- const enclosingAnnotation = enclosingAnnotationSpan?.annotation;
-
- if (enclosingAnnotation && !existingAnnotations.includes(enclosingAnnotation))
- existingAnnotations.push(enclosingAnnotation);
- });
-
- if (existingAnnotations.length > 0)
- return existingAnnotations.filter(anno => {
- const isSameAnchor = anno.anchor == newAnnotation.anchor;
- const isSameQuote = anno.quote == newAnnotation.quote;
- return isSameAnchor && isSameQuote;
- });
- else
- return [];
-};
-
-export const enableTouch = (element, selectHandler) => {
- let touchTimeout;
- let lastTouchEvent;
-
- const onTouchStart = evt => {
- if (!touchTimeout) {
- lastTouchEvent = evt;
- touchTimeout = setTimeout(executeTouchSelect, 1000);
- }
- }
-
- const executeTouchSelect = () => {
- if (lastTouchEvent) {
- selectHandler(lastTouchEvent);
- touchTimeout = null;
- lastTouchEvent = null;
- }
- };
-
- const resetTouch = evt => {
- if (touchTimeout) {
- clearTimeout(touchTimeout);
- touchTimeout = setTimeout(executeTouchSelect, 1500);
- }
- }
-
- element.addEventListener('touchstart', onTouchStart);
- document.addEventListener('selectionchange', resetTouch);
-}
\ No newline at end of file
diff --git a/src/selection/index.js b/src/selection/index.js
index bb26bf9..b002fe4 100644
--- a/src/selection/index.js
+++ b/src/selection/index.js
@@ -1,2 +1,2 @@
export { default as Selection } from './Selection';
-export { default as SelectionHandler } from './SelectionHandler';
+