Bulk Labeling for Text Spans with Keyboard Shortcut

note
For information about modifying this plugin or creating your own custom plugins, see Customize and Build Your Own Plugins.
For general plugin information, see Plugins for projects and Plugin FAQ.
About
This plugin automatically applies the same label to all matching text spans when you press the Shift key.
For example, if you apply the PER
label to the text span Smith
, this plugin will automatically find all instances of Smith
in the text and apply the PER
label to them.
- Shift key tracking
- The plugin tracks the state of the Shift key using
keydown
andkeyup
event listeners. A boolean variableisShiftKeyPressed
is set totrue
when the Shift key is pressed andfalse
when it is released.
- The plugin tracks the state of the Shift key using
- Bulk deletion
- When a region (annotation) is deleted and the Shift key is pressed, the plugin identifies all regions with the same text and label as the deleted region.
- It then deletes all these matching regions to facilitate bulk deletion.
- Bulk creation
- When a region is created and the Shift key is pressed, the plugin searches for all occurrences of the created region’s text within the document.
- It creates new regions for each occurrence of the text, ensuring that no duplicate regions are created (i.e., regions with overlapping start and end offsets are avoided).
- The plugin also prevents tagging of single characters to avoid unnecessary annotations.
- Timeout
- To prevent rapid consecutive bulk operations, the plugin sets a timeout of 1 second. This ensures that bulk operations are not triggered too frequently.
Plugin
/**
* Automatically creates text regions for all instances of the selected text and deletes existing regions
* when the shift key is pressed.
*/
// Track the state of the shift key
let isShiftKeyPressed = false;
window.addEventListener("keydown", (e) => {
if (e.key === "Shift") {
isShiftKeyPressed = true;
}
});
window.addEventListener("keyup", (e) => {
if (e.key === "Shift") {
isShiftKeyPressed = false;
}
});
LSI.on("entityDelete", (region) => {
if (!isShiftKeyPressed) return; // Only proceed if the shift key is pressed
if (window.BULK_REGIONS) return;
window.BULK_REGIONS = true;
setTimeout(() => {
window.BULK_REGIONS = false;
}, 1000);
const existingEntities = Htx.annotationStore.selected.regions;
const regionsToDelete = existingEntities.filter((entity) => {
const deletedText = region.text.toLowerCase().replace("\\\\n", " ");
const otherText = entity.text.toLowerCase().replace("\\\\n", " ");
return deletedText === otherText && region.labels[0] === entity.labels[0];
});
for (const region of regionsToDelete) {
Htx.annotationStore.selected.deleteRegion(region);
}
Htx.annotationStore.selected.updateObjects();
});
LSI.on("entityCreate", (region) => {
if (!isShiftKeyPressed) return;
if (window.BULK_REGIONS) return;
window.BULK_REGIONS = true;
setTimeout(() => {
window.BULK_REGIONS = false;
}, 1000);
const existingEntities = Htx.annotationStore.selected.regions;
setTimeout(() => {
// Prevent tagging a single character
if (region.text.length < 2) return;
regexp = new RegExp(
region.text.replace("\\\\n", "\\\\s+").replace(" ", "\\\\s+"),
"gi",
);
const matches = Array.from(region.object._value.matchAll(regexp));
for (const match of matches) {
if (match.index === region.startOffset) continue;
const startOffset = match.index;
const endOffset = match.index + region.text.length;
// Check for existing entities with overlapping start and end offset
let isDuplicate = false;
for (const entity of existingEntities) {
if (
startOffset <= entity.globalOffsets.end &&
entity.globalOffsets.start <= endOffset
) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
Htx.annotationStore.selected.createResult(
{
text: region.text,
start: "/span[1]/text()[1]",
startOffset: startOffset,
end: "/span[1]/text()[1]",
endOffset: endOffset,
},
{
labels: [...region.labeling.value.labels],
},
region.labeling.from_name,
region.object,
);
}
}
Htx.annotationStore.selected.updateObjects();
}, 100);
});
Related LSI instance methods:
Related frontend APIs:
Related frontend events:
Labeling config
This is a basic NER labeling config.
<View>
<Labels name="label" toName="text">
<Label value="PER" background="red"/>
<Label value="ORG" background="darkorange"/>
<Label value="LOC" background="orange"/>
<Label value="MISC" background="green"/>
</Labels>
<Text name="text" value="$text"/>
</View>
Related tags:
Sample data
[
{
"data": {
"text": "Opossums are nocturnal animals that are often seen scavenging for food at night. They have a prehensile tail that helps them climb trees and navigate their environment."
}
},
{
"data": {
"text": "Opossums are known for their ability to play dead when threatened, a behavior known as 'playing possum'. This act can deter predators and give the opossum a chance to escape."
}
},
{
"data": {
"text": "Opossums are marsupials, meaning they carry and nurse their young in a pouch. Baby opossums, called joeys, stay in the pouch for about two months after birth."
}
}
]