label-studio
Version:
Data Labeling Tool that is backend agnostic and can be embedded into your applications
390 lines (319 loc) • 9.84 kB
JavaScript
import emojiRegex from "emoji-regex";
import React, { Component } from "react";
import { observer } from "mobx-react";
import Utils from "../../utils";
import Range from "./Range";
import { HtxTextNode } from "./Node";
import UrlNode from "./UrlNode";
import EmojiNode from "./EmojiNode";
import styles from "./TextHighlight.module.scss";
class TextHighlight extends Component {
constructor() {
super();
this.dismissMouseUp = 0;
}
/**
* Return first ok element
*/
getRange(charIndex) {
if (this.props.ranges && this.props.ranges.length) {
return this.props.ranges.find(range => charIndex >= range.start && charIndex <= range.end);
}
}
/**
* Function when the user mouse is over an highlighted text
*/
onMouseOverHighlightedWord(range, visible) {
if (visible && this.props.onMouseOverHighlightedWord) {
this.props.onMouseOverHighlightedWord(range);
}
}
getLetterNode(charIndex, range) {
/**
* Current symbol
*/
const char = this.props.text[charIndex];
let nl;
/**
* Line break
*/
if (char && char.charCodeAt()) {
nl = char.charCodeAt(0) === 10;
}
let arrOverlap = [];
if (this.props.ranges) {
this.props.ranges.map(range => {
if (charIndex >= range.start && charIndex <= range.end) {
return (arrOverlap = [...arrOverlap, range.id]);
}
return arrOverlap;
});
}
return (
<HtxTextNode
id={this.props.id}
overlap={arrOverlap}
range={range}
charIndex={charIndex}
key={`${this.props.id}-${charIndex}`}
highlightStyle={this.props.highlightStyle}
// style={{padding: "2px 0", background: "linear-gradient(180deg, #fdcd3b 60%, #ffed4b 60%, red 9%)"}}
>
{nl ? <br /> : char}
</HtxTextNode>
);
}
getEmojiNode(charIndex, range) {
let arrOverlap = [];
if (this.props.ranges) {
this.props.ranges.map(range => {
if (charIndex >= range.start && charIndex <= range.end) {
return (arrOverlap = [...arrOverlap, range.id]);
}
return arrOverlap;
});
}
return (
<EmojiNode
text={this.props.text}
id={this.props.id}
overlap={arrOverlap}
range={range}
key={`${this.props.id}-emoji-${charIndex}`}
charIndex={charIndex}
highlightStyle={this.props.highlightStyle}
/>
);
}
getUrlNode(charIndex, range, url) {
let arrOverlap = [];
if (this.props.ranges) {
this.props.ranges.map(range => {
if (charIndex >= range.start && charIndex <= range.end) {
return (arrOverlap = [...arrOverlap, range.id]);
}
return arrOverlap;
});
}
return (
<UrlNode
url={url}
id={this.props.id}
overlap={arrOverlap}
range={range}
key={`${this.props.id}-url-${charIndex}`}
charIndex={charIndex}
highlightStyle={this.props.highlightStyle}
/>
);
}
mouseEvent() {
if (!this.props.enabled) {
return false;
}
let text = "";
if (window.getSelection) {
/**
* Get highlited text
* Tip: with helper elements (hints)
*/
// text = window.getSelection().toString();
if (window.getSelection().type === "None") return;
/**
* Create clone range
*/
let cloneCont = window
.getSelection()
.getRangeAt(0)
.cloneRange();
/**
* The Range.cloneContents() returns a DocumentFragment copying the objects of type Node included in the Range.
*/
let selectionContents = cloneCont.cloneContents();
/**
* Create virtual div with text
*/
let virtualDiv = document.createElement("div");
virtualDiv.appendChild(selectionContents);
let elementsWithSup = virtualDiv.getElementsByTagName("sup");
if (elementsWithSup.length > 0) {
for (let i = 0; i < elementsWithSup.length; i++) {
elementsWithSup[i].innerText = "";
}
text = virtualDiv.innerText;
} else {
text = virtualDiv.innerText;
}
} else if (document.selection && document.selection.type !== "Control") {
text = document.selection.createRange().text;
}
if (!text || !text.length) return false;
const range = window.getSelection().getRangeAt(0);
/**
* Check for hint
*/
if (range.startContainer.parentNode.dataset.hint || range.endContainer.parentNode.dataset.hint) return;
/**
* Start position of selected item
*/
let startContainerPosition = parseInt(range.startContainer.parentNode.dataset.position);
/**
* End position of selected item
*/
let endContainerPosition = parseInt(range.endContainer.parentNode.dataset.position);
if (!range.startContainer.parentNode.dataset.position) {
if (!range.startContainer.dataset) return;
startContainerPosition = parseInt(range.startContainer.dataset.position);
}
if (!range.endContainer.parentNode.dataset.position) {
if (!range.endContainer.dataset) return;
endContainerPosition = parseInt(range.endContainer.dataset.position);
}
let startHL = startContainerPosition < endContainerPosition ? startContainerPosition : endContainerPosition;
let endHL = startContainerPosition < endContainerPosition ? endContainerPosition : startContainerPosition;
const rangeObj = new Range(startHL, endHL, text, {
...this.props,
ranges: undefined,
});
this.props.onTextHighlighted(rangeObj);
}
/**
*
* @param {*} event
*/
onMouseUp(event) {
this.mouseEvent.bind(this)();
}
onMouseDown(event) {
// console.log(event)
}
onMouseEnter(event) {
// console.log(event)
}
/**
* Double click on text
* @param {*} event
*/
onDoubleClick(event) {
// WARN
// event.stopPropagation();
// this.doucleckicked = true;
// this.mouseEvent.bind(this)();
}
/**
* @param {object} letterGroup All marked sections of text
* @param {object} range Range of marked section
* @param {number} textCharIndex The last number of selection
* @param {function} onMouseOverHighlightedWord
*/
rangeRenderer(letterGroup, range, textCharIndex, onMouseOverHighlightedWord) {
if (this.props.rangeRenderer) {
return this.props.rangeRenderer(letterGroup, range, textCharIndex, onMouseOverHighlightedWord);
}
return letterGroup;
}
getNode(i, range, text, url, isEmoji) {
if (url.length) {
return this.getUrlNode(i, range, url);
} else if (isEmoji) {
return this.getEmojiNode(i, range);
}
return this.getLetterNode(i, range);
}
getRanges() {
/**
* Text with nodes
*/
const newText = [];
let lastRange;
/**
* For all the characters on the text
*/
for (let textCharIndex = 0; textCharIndex < this.props.text.length; textCharIndex++) {
/**
* Get range text
*/
const range = this.getRange(textCharIndex);
/**
* Check characters for URL
*/
const url = Utils.Checkers.getUrl(textCharIndex, this.props.text);
/**
* Check characters for emoji
*/
const isEmoji = emojiRegex().test(this.props.text[textCharIndex] + this.props.text[textCharIndex + 1]);
/**
* Get the current character node
*/
const node = this.getNode(textCharIndex, range, this.props.text, url, isEmoji);
/**
* If the next node is an url one, we fast forward to the end of it
*/
if (url.length) {
textCharIndex += url.length - 1;
} else if (isEmoji) {
/**
* Because an emoji is composed of 2 chars
*/
textCharIndex++;
}
if (!range) {
newText.push(node);
continue;
}
/**
* If the char is in range
*/
lastRange = range;
// console.log(this.props.text[lastRange.start], this.props.text[lastRange.end])
/**
* We put the first range node on the array
*/
const letterGroup = [node];
/**
* For all the characters in the highlighted range
*/
let rangeCharIndex = textCharIndex + 1;
// if (rangeCharIndex >= parseInt(range.start) && rangeCharIndex <= parseInt(range.end)) {
// console.log(this.props.text[parseInt(range.end)])
// }
// console.log(textCharIndex, range.start, range.end)
for (; rangeCharIndex < parseInt(range.end) + 1; rangeCharIndex++) {
/**
* Emoji handler
*/
const isEmoji = emojiRegex().test(`${this.props.text[rangeCharIndex]}${this.props.text[rangeCharIndex + 1]}`);
if (isEmoji) {
letterGroup.push(this.getEmojiNode(rangeCharIndex, range));
// Because an emoji is composed of 2 chars
rangeCharIndex++;
} else {
letterGroup.push(this.getLetterNode(rangeCharIndex, range));
}
textCharIndex = rangeCharIndex;
}
newText.push(this.rangeRenderer(letterGroup, range, textCharIndex, this.onMouseOverHighlightedWord.bind(this)));
}
if (lastRange) {
// Callback function
this.onMouseOverHighlightedWord(lastRange, true);
}
return newText;
}
render() {
const newText = this.getRanges();
return (
<div
className={styles.block}
style={this.props.style}
onMouseUp={this.onMouseUp.bind(this)}
onMouseDown={this.onMouseDown.bind(this)}
onMouseEnter={this.onMouseEnter.bind(this)}
onDoubleClick={this.onDoubleClick.bind(this)}
>
{newText}
</div>
);
}
}
export default observer(TextHighlight);