@recogito/recogito-js
Version:
A JavaScript library for text annotation
142 lines (114 loc) • 4.28 kB
JavaScript
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;
this.document = element.ownerDocument;
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 => {
// left click only
if (evt.button === 0)
this.clearSelection();
}
_onMouseUp = evt => {
if (this.isEnabled) {
const selection = this.document.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
const top = exactOverlaps[0];
this.clearSelection();
this.emit('select', {
selection: top,
element: this.document.querySelector(`.r6o-annotation[data-id="${top.id}"]`)
});
} else {
this.emit('select', {
selection: stub,
element: selectedRange
});
}
}
}
}
}
_hideNativeSelection = () => {
this.el.classList.add('r6o-hide-selection');
}
clearSelection = () => {
if (this.isEnabled) {
this._currentSelection = null;
// Remove native selection, if any
if (this.document.getSelection) {
if (this.document.getSelection().empty) { // Chrome
this.document.getSelection().empty();
} else if (this.document.getSelection().removeAllRanges) { // Firefox
this.document.getSelection().removeAllRanges();
}
} else if (this.document.selection) { // IE?
this.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(this.document.createTextNode(span.textContent), span);
parent.removeChild(span);
});
}
this.el.normalize();
}
}
}