Skip to content

Commit 3408e0b

Browse files
authored
Merge pull request #6109 from inception-project/feature/6108-Relation-support-for-the-Apache-Annotator-HTML-editor-mode
#6108 - Relation support for the Apache Annotator HTML editor mode
2 parents 13d28e0 + 45a63f2 commit 3408e0b

17 files changed

Lines changed: 2626 additions & 123 deletions

File tree

inception/inception-assistant-ui/src/main/ts_template/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
"type": "git",
2020
"url": "git+https://github.com/inception-project/inception.git"
2121
},
22-
"bugs": {
23-
"url": "https://github.com/inception-project/issues"
24-
},
2522
"overrides": {
2623
"minimatch": "${minimatch.version}",
2724
"immutable": "${immutable.version}",

inception/inception-html-apache-annotator-editor/src/main/java/de/tudarmstadt/ukp/inception/apacheannotatoreditor/ApacheAnnotatorHtmlAnnotationEditorFactoryUserPreferences.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
},
2929
"keyboardCursorEnabled": {
3030
"type": "boolean"
31+
},
32+
"showRelationLabels": {
33+
"type": "boolean"
34+
},
35+
"lineSpacing": {
36+
"type": "string",
37+
"enum": ["low", "mid", "high", "xhigh"]
3138
}
3239
},
3340
"additionalProperties": false

inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorEditor.scss

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,33 @@ sec-wrap {
113113

114114
.i7n-wrapper {
115115
position: relative;
116-
line-height: 1.8;
116+
line-height: 1.8; // default; overridden by the .iaa-line-spacing-* presets below
117117
overflow: auto;
118118
padding-left: var(--vertical-tray-width-left);
119119
padding-right: var(--vertical-tray-width-right);
120120
}
121121

122+
/**
123+
* Line-spacing presets. Toggled as a class on the editor root by applyLineSpacing();
124+
* line-height inherits down to the document content.
125+
*/
126+
.iaa-line-spacing-low {
127+
line-height: 1.4;
128+
}
129+
130+
.iaa-line-spacing-mid {
131+
line-height: 1.8;
132+
}
133+
134+
.iaa-line-spacing-high {
135+
line-height: 2.6;
136+
}
137+
138+
.iaa-line-spacing-xhigh {
139+
line-height: 5.2; // double the `high` preset; the arc bow scale is already capped (see
140+
// ARC_BOW_SCALE_MAX) so the stacked-arc bow matches `high` rather than scaling further.
141+
}
142+
122143
/**
123144
* Annotation highlight.
124145
*/

inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorEditor.ts

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ import { ApacheAnnotatorVisualizer } from './ApacheAnnotatorVisualizer.svelte';
2727
import { ApacheAnnotatorSelector } from './ApacheAnnotatorSelector';
2828
import { normalizeSectionsForPinning } from './SectionPinning';
2929
import ApacheAnnotatorToolbar from './ApacheAnnotatorToolbar.svelte';
30-
import { annotatorState } from './ApacheAnnotatorState.svelte';
30+
import {
31+
annotatorState,
32+
defaultAnnotatorPreferences as defaultPreferences,
33+
LINE_SPACINGS,
34+
type LineSpacing,
35+
} from './ApacheAnnotatorState.svelte';
3136
import AnnotationDetailPopOver from '@inception-project/inception-js-api/src/widget/AnnotationDetailPopOver.svelte';
3237
import { mount, tick, unmount } from 'svelte';
3338
import {
@@ -71,24 +76,13 @@ export class ApacheAnnotatorEditor implements AnnotationEditor {
7176
this.ajax = ajax;
7277
this.root = element;
7378
this.userPreferencesKey = userPreferencesKey;
74-
// Settled during init by normalizeSectionsForPinning (Step 1 below).
79+
// Settled during init by normalizeSectionsForPinning.
7580
this.sectionSelector = '';
7681
this.protectedElements = protectedElements;
7782
this.documentStructure = documentStructure;
7883
const protectedSel = [...protectedElements].join(',');
7984
this.protectedElementsMatcher = compileNsSelector(protectedSel) || undefined;
8085

81-
const defaultPreferences = {
82-
showLabels: true,
83-
showAggregatedLabels: false,
84-
showEmptyHighlights: false,
85-
showDocumentStructure: false,
86-
showImages: true,
87-
showTables: true,
88-
documentStructureWidth: 0.2,
89-
protectElements: true,
90-
keyboardCursorEnabled: false,
91-
};
9286
let preferences = Object.assign({}, defaultPreferences);
9387

9488
ajax.loadPreferences(userPreferencesKey)
@@ -97,6 +91,8 @@ export class ApacheAnnotatorEditor implements AnnotationEditor {
9791
console.log('Loaded preferences', preferences);
9892

9993
annotatorState.showLabels = preferences.showLabels ?? defaultPreferences.showLabels;
94+
annotatorState.showRelationLabels =
95+
preferences.showRelationLabels ?? defaultPreferences.showRelationLabels;
10096
annotatorState.showAggregatedLabels =
10197
preferences.showAggregatedLabels ?? defaultPreferences.showAggregatedLabels;
10298
annotatorState.showEmptyHighlights =
@@ -111,55 +107,57 @@ export class ApacheAnnotatorEditor implements AnnotationEditor {
111107
preferences.protectElements ?? defaultPreferences.protectElements;
112108
annotatorState.keyboardCursorEnabled =
113109
preferences.keyboardCursorEnabled ?? defaultPreferences.keyboardCursorEnabled;
110+
const loadedLineSpacing = preferences.lineSpacing ?? defaultPreferences.lineSpacing;
111+
annotatorState.lineSpacing = LINE_SPACINGS.includes(
112+
loadedLineSpacing as LineSpacing
113+
)
114+
? (loadedLineSpacing as LineSpacing)
115+
: 'mid';
116+
this.applyLineSpacing();
114117
})
115118
.then(() => {
116-
// Move all content into a document container
117119
const documentContainer = this.root.ownerDocument.createElement('div');
118120
documentContainer.classList.add('iaa-document-container');
119121
[...this.root.ownerDocument.body.children].forEach((child) =>
120122
documentContainer.appendChild(child)
121123
);
122124

123-
// Step 1 (rendering fix): make section elements pinnable and
124-
// settle the section selector. Schema-free; see SectionPinning.
125-
// Must run before the IDs are assigned so they land on the
126-
// wrapper elements rather than the soon-to-be-buried originals.
125+
// Make section elements pinnable and settle the section selector. This must run
126+
// before the IDs are assigned so they land on the wrapper elements rather than the
127+
// soon-to-be-buried originals.
127128
this.sectionSelector = normalizeSectionsForPinning(
128129
documentContainer,
129130
sectionElementLocalNames
130131
);
131132
this.ensureSectionElementsHaveAnId();
132133

133-
// Step 2 (table of contents): build the outline for the
134-
// navigator. Operates on the post-pinning DOM; owns title and
135-
// scroll-target extraction per format.
134+
// Build the outline for the navigator, operating on the post-pinning DOM.
136135
this.documentStructure.preprocess(documentContainer);
137136

138-
// Set up a container for the document navigation sidebar
139137
this.navigatorContainer = this.root.ownerDocument.createElement('div');
140138
this.navigatorContainer.classList.add('iaa-document-navigator');
141139

142-
// Set up a split pane to host the document and the document structure navigation sidebar
143140
this.horizSplitPane = this.createHorizontalSplitPane(
144141
this.navigatorContainer,
145142
documentContainer
146143
);
147144

148-
// Add the split pane to the document
149145
this.root.ownerDocument.body.appendChild(this.horizSplitPane);
150146

151-
// Add the editor components for the document container
147+
// The selector is created first so the visualizer's relation-drag controller can
148+
// defer multi-hit target disambiguation to it.
149+
this.selector = new ApacheAnnotatorSelector(this.root, this.ajax);
152150
this.vis = new ApacheAnnotatorVisualizer(
153151
this.root,
154152
this.ajax,
155-
this.sectionSelector
153+
this.sectionSelector,
154+
(clientX, clientY, targets, onPick) =>
155+
this.selector.pickRelationTarget(clientX, clientY, targets, onPick)
156156
);
157157
this.vis.protectedElementSelector = [...protectedElements].join(',');
158-
this.selector = new ApacheAnnotatorSelector(this.root, this.ajax);
159158

160159
this.documentContainer = documentContainer;
161160

162-
// Add auxiliary controls
163161
this.documentStructureNavigator = this.createDocumentNavigator(
164162
this.navigatorContainer,
165163
documentContainer,
@@ -184,10 +182,8 @@ export class ApacheAnnotatorEditor implements AnnotationEditor {
184182
},
185183
});
186184

187-
// Event handler for creating an annotion or selecting an annotation
188185
this.root.addEventListener('mouseup', (e) => this.onMouseUp(e));
189186

190-
// Event handler for opening the context menu
191187
this.root.addEventListener('contextmenu', (e) => this.onRightClick(e));
192188

193189
// Prevent right-click from triggering a selection event
@@ -233,7 +229,6 @@ export class ApacheAnnotatorEditor implements AnnotationEditor {
233229
divider.addEventListener('mousedown', (e) => {
234230
if (e.buttons !== 1) return;
235231

236-
// Clean up any previous resize operation
237232
if (this.activeResizeCleanup) {
238233
this.activeResizeCleanup();
239234
}
@@ -357,10 +352,37 @@ export class ApacheAnnotatorEditor implements AnnotationEditor {
357352
keyboardCursorPreferencesChanged: (e: Event) => {
358353
this.keyboardMode?.enable(annotatorState.keyboardCursorEnabled);
359354
},
355+
lineSpacingPreferencesChanged: (e: Event) => {
356+
this.applyLineSpacing();
357+
// The new line-height reflows the text, moving every highlight. Re-materialize
358+
// the locked section dimensions and re-render so arcs/stubs follow the new
359+
// positions. Only the geometry changed — the annotation data is unchanged — so
360+
// re-render from the cached data instead of a full server round-trip.
361+
this.vis?.optimizeLayout('line spacing');
362+
this.vis?.rerender();
363+
},
364+
relationLabelPreferencesChanged: (e: Event) => {
365+
// Pure visual toggle — the labels are already drawn, so just flip the mode.
366+
this.vis?.refreshRelationLabelMode();
367+
},
360368
},
361369
});
362370
}
363371

372+
/**
373+
* Apply the active line-spacing preset as a class on the editor root, matching the
374+
* class-toggle pattern used by the other visual preferences (e.g. `iaa-hide-images`).
375+
* The heights live in SCSS; `line-height` inherits down to the document content.
376+
*/
377+
private applyLineSpacing(): void {
378+
const root = this.root as HTMLElement;
379+
LINE_SPACINGS.forEach((s) => root.classList.remove(`iaa-line-spacing-${s}`));
380+
const spacing = LINE_SPACINGS.includes(annotatorState.lineSpacing)
381+
? annotatorState.lineSpacing
382+
: 'mid';
383+
root.classList.add(`iaa-line-spacing-${spacing}`);
384+
}
385+
364386
private createDocumentNavigator(
365387
target: HTMLElement,
366388
documentContainer: HTMLElement,

inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorSelector.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ import { createPopper, type Instance } from '@popperjs/core';
2828
* selection is offered (no ❌); activating an entry selects it.
2929
* - `keyboard-delete`: opened via the keyboard to pick an annotation to delete.
3030
* Activating an entry deletes it and the entry is styled as destructive.
31+
* - `relation-target`: opened by a relation-creation drag that ended on a stack of
32+
* several spans. Activating an entry resolves the
33+
* pending pick callback with the chosen vid instead of selecting/deleting; dismissing
34+
* the popup cancels the relation. No ❌ buttons.
3135
*/
32-
type PopupMode = 'mouse' | 'keyboard-select' | 'keyboard-delete';
36+
type PopupMode = 'mouse' | 'keyboard-select' | 'keyboard-delete' | 'relation-target';
3337

3438
export class ApacheAnnotatorSelector {
3539
private ajax: DiamAjax;
@@ -45,6 +49,10 @@ export class ApacheAnnotatorSelector {
4549
private focusFallback: HTMLElement | undefined;
4650
private popupKeydownHandler: ((e: KeyboardEvent) => void) | undefined;
4751
private popupMode: PopupMode = 'mouse';
52+
// In 'relation-target' mode, the drag controller's callback to resolve with the
53+
// chosen target vid. Cleared (without being called) when the popup is dismissed,
54+
// which the controller treats as a cancel.
55+
private pendingTargetPick: ((vid: VID) => void) | undefined;
4856

4957
public constructor(element: Element, ajax: DiamAjax) {
5058
this.ajax = ajax;
@@ -111,8 +119,24 @@ export class ApacheAnnotatorSelector {
111119
return true;
112120
}
113121

122+
/**
123+
* Ask the user which of several overlapping spans is a relation's target. The popup
124+
* opens at the drop point; `onPick` fires with the chosen vid, or not at all if the
125+
* popup is dismissed (which the caller treats as a cancel). `hls` must already be deduplicated by vid.
126+
*/
127+
public pickRelationTarget(
128+
clientX: number,
129+
clientY: number,
130+
hls: HTMLElement[],
131+
onPick: (vid: VID) => void
132+
): void {
133+
this.destroyPopup();
134+
if (hls.length === 0) return;
135+
this.pendingTargetPick = onPick;
136+
this.createPopup(clientX, clientY, hls, true, 'relation-target');
137+
}
138+
114139
private onMouseDown(event: Event): void {
115-
// Destroy popup if clicked outside of popup
116140
if (!this.isVisible()) {
117141
return;
118142
}
@@ -141,6 +165,7 @@ export class ApacheAnnotatorSelector {
141165
this.activeIndex = -1;
142166
this.focusFallback = undefined;
143167
this.popupMode = 'mouse';
168+
this.pendingTargetPick = undefined;
144169
}
145170

146171
public isVisible(): boolean {
@@ -153,7 +178,6 @@ export class ApacheAnnotatorSelector {
153178
this.destroyPopup();
154179

155180
const hls = event.target instanceof Node ? highlights(event.target) : [];
156-
// No need to show selector if there is no annotation
157181
if (hls.length === 0) return;
158182

159183
if (
@@ -205,12 +229,16 @@ export class ApacheAnnotatorSelector {
205229
// Make the menu focusable so it can capture keyboard navigation keys.
206230
this.popupContent.tabIndex = -1;
207231

208-
// In the keyboard modes, show a header indicating whether picking an entry
209-
// selects or deletes it.
232+
// Outside plain mouse mode, show a header indicating what activating an entry does.
210233
if (mode !== 'mouse') {
211234
const header = document.createElement('div');
212235
header.classList.add('iaa-menu-header');
213-
header.textContent = mode === 'keyboard-delete' ? 'Delete…' : 'Select…';
236+
header.textContent =
237+
mode === 'keyboard-delete'
238+
? 'Delete…'
239+
: mode === 'relation-target'
240+
? 'Relation target…'
241+
: 'Select…';
214242
this.popupContent.appendChild(header);
215243
}
216244

@@ -303,16 +331,27 @@ export class ApacheAnnotatorSelector {
303331

304332
/**
305333
* Perform the popup's primary action on an entry: delete in keyboard-delete mode,
306-
* otherwise select.
334+
* resolve the relation-target pick in relation-target mode, otherwise select.
307335
*/
308336
private activateItem(event: Event, id: VID) {
309337
if (this.popupMode === 'keyboard-delete') {
310338
this.onDeleteAnnotation(event, id);
339+
} else if (this.popupMode === 'relation-target') {
340+
this.onPickRelationTarget(event, id);
311341
} else {
312342
this.onSelectAnnotation(event, id);
313343
}
314344
}
315345

346+
private onPickRelationTarget(event: Event, id: VID) {
347+
event.stopPropagation();
348+
// Capture before destroyPopup() clears it: destroy first so the controller's
349+
// createRelationAnnotation runs against a torn-down popup, then resolve.
350+
const pick = this.pendingTargetPick;
351+
this.destroyPopup();
352+
pick?.(id);
353+
}
354+
316355
private onSelectAnnotation(event: Event, id: VID) {
317356
console.log(`Selecting annotation ${id}`);
318357
event.stopPropagation();

inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorState.svelte.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@
1616
* limitations under the License.
1717
*/
1818

19-
export const annotatorState = $state({
19+
/**
20+
* Line-spacing presets. The actual line-heights live in SCSS next to the
21+
* `.i7n-wrapper` rule (`iaa-line-spacing-low|mid|high|xhigh`); `mid` is the default.
22+
*/
23+
export type LineSpacing = 'low' | 'mid' | 'high' | 'xhigh';
24+
export const LINE_SPACINGS: readonly LineSpacing[] = ['low', 'mid', 'high', 'xhigh'];
25+
26+
export const defaultAnnotatorPreferences = {
2027
showLabels: true,
28+
showRelationLabels: true,
2129
showAggregatedLabels: false,
2230
showEmptyHighlights: false,
2331
showDocumentStructure: false,
@@ -26,4 +34,7 @@ export const annotatorState = $state({
2634
showTables: true,
2735
protectElements: true,
2836
keyboardCursorEnabled: false,
29-
});
37+
lineSpacing: 'mid' as LineSpacing,
38+
};
39+
40+
export const annotatorState = $state({ ...defaultAnnotatorPreferences });

0 commit comments

Comments
 (0)