@semiont/react-ui 0.2.35-build.97 → 0.2.35-build.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +23 -15
- package/dist/index.mjs +103 -123
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CodeMirrorRenderer.tsx +73 -65
- package/src/components/resource/AnnotateView.tsx +9 -6
- package/src/components/resource/BrowseView.tsx +0 -1
- package/src/components/resource/ResourceViewer.tsx +0 -2
- package/src/components/resource/__tests__/BrowseView.test.tsx +0 -1
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { EditorView, Decoration, DecorationSet, lineNumbers } from '@codemirror/
|
|
|
5
5
|
import { EditorState, RangeSetBuilder, StateField, StateEffect, Compartment } from '@codemirror/state';
|
|
6
6
|
import { markdown } from '@codemirror/lang-markdown';
|
|
7
7
|
import { ANNOTATORS } from '../lib/annotation-registry';
|
|
8
|
-
import { ReferenceResolutionWidget } from '../lib/codemirror-widgets';
|
|
8
|
+
import { ReferenceResolutionWidget, showWidgetPreview, hideWidgetPreview } from '../lib/codemirror-widgets';
|
|
9
9
|
import { scrollAnnotationIntoView } from '../lib/scroll-utils';
|
|
10
10
|
import { isHighlight, isReference, isResolvedReference, isComment, isAssessment, isTag, getBodySource } from '@semiont/api-client';
|
|
11
11
|
import type { components } from '@semiont/core';
|
|
@@ -35,7 +35,6 @@ interface Props {
|
|
|
35
35
|
editable?: boolean;
|
|
36
36
|
newAnnotationIds?: Set<string>;
|
|
37
37
|
hoveredAnnotationId?: string | null;
|
|
38
|
-
hoveredCommentId?: string | null;
|
|
39
38
|
scrollToAnnotationId?: string | null;
|
|
40
39
|
sourceView?: boolean; // If true, show raw source (no markdown rendering)
|
|
41
40
|
showLineNumbers?: boolean; // If true, show line numbers
|
|
@@ -59,7 +58,6 @@ interface WidgetUpdate {
|
|
|
59
58
|
content: string;
|
|
60
59
|
segments: TextSegment[];
|
|
61
60
|
generatingReferenceId?: string | null | undefined;
|
|
62
|
-
eventBus?: EventBus;
|
|
63
61
|
getTargetDocumentName?: (documentId: string) => string | undefined;
|
|
64
62
|
}
|
|
65
63
|
|
|
@@ -88,11 +86,16 @@ function convertSegmentPositions(segments: TextSegment[], content: string): Text
|
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
//
|
|
89
|
+
// Binary search: count CRLFs before a position in O(log n)
|
|
92
90
|
const convertPosition = (pos: number): number => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
let lo = 0;
|
|
92
|
+
let hi = crlfPositions.length;
|
|
93
|
+
while (lo < hi) {
|
|
94
|
+
const mid = (lo + hi) >>> 1;
|
|
95
|
+
if (crlfPositions[mid]! < pos) lo = mid + 1;
|
|
96
|
+
else hi = mid;
|
|
97
|
+
}
|
|
98
|
+
return pos - lo;
|
|
96
99
|
};
|
|
97
100
|
|
|
98
101
|
return segments.map(seg => ({
|
|
@@ -203,13 +206,10 @@ function buildWidgetDecorations(
|
|
|
203
206
|
_content: string,
|
|
204
207
|
segments: TextSegment[],
|
|
205
208
|
generatingReferenceId: string | null | undefined,
|
|
206
|
-
eventBus: any,
|
|
207
209
|
getTargetDocumentName?: (documentId: string) => string | undefined
|
|
208
210
|
): DecorationSet {
|
|
209
211
|
const builder = new RangeSetBuilder<Decoration>();
|
|
210
212
|
|
|
211
|
-
// Wiki link widgets removed (WikiLinkWidget was deleted)
|
|
212
|
-
|
|
213
213
|
// Process all annotations (references and highlights) in sorted order
|
|
214
214
|
// This ensures decorations are added in the correct order for CodeMirror
|
|
215
215
|
const allAnnotatedSegments = segments
|
|
@@ -222,20 +222,17 @@ function buildWidgetDecorations(
|
|
|
222
222
|
const annotation = segment.annotation;
|
|
223
223
|
|
|
224
224
|
// For references: add resolution widget (🔗, ✨ pulsing, or ❓)
|
|
225
|
-
// Use W3C helper to determine if this is a reference
|
|
226
225
|
if (isReference(annotation)) {
|
|
227
226
|
const bodySource = getBodySource(annotation.body);
|
|
228
227
|
const targetName = bodySource
|
|
229
228
|
? getTargetDocumentName?.(bodySource)
|
|
230
229
|
: undefined;
|
|
231
|
-
// Compare by ID portion (handle both URI and internal ID formats)
|
|
232
230
|
const isGenerating = generatingReferenceId
|
|
233
231
|
? annotation.id === generatingReferenceId
|
|
234
232
|
: false;
|
|
235
233
|
const widget = new ReferenceResolutionWidget(
|
|
236
234
|
annotation,
|
|
237
235
|
targetName,
|
|
238
|
-
eventBus,
|
|
239
236
|
isGenerating
|
|
240
237
|
);
|
|
241
238
|
builder.add(
|
|
@@ -264,7 +261,6 @@ const widgetDecorationsField = StateField.define<DecorationSet>({
|
|
|
264
261
|
effect.value.content,
|
|
265
262
|
effect.value.segments,
|
|
266
263
|
effect.value.generatingReferenceId,
|
|
267
|
-
effect.value.eventBus,
|
|
268
264
|
effect.value.getTargetDocumentName
|
|
269
265
|
);
|
|
270
266
|
}
|
|
@@ -282,7 +278,6 @@ export function CodeMirrorRenderer({
|
|
|
282
278
|
editable = false,
|
|
283
279
|
newAnnotationIds,
|
|
284
280
|
hoveredAnnotationId,
|
|
285
|
-
hoveredCommentId,
|
|
286
281
|
scrollToAnnotationId,
|
|
287
282
|
sourceView = false,
|
|
288
283
|
showLineNumbers = false,
|
|
@@ -301,12 +296,19 @@ export function CodeMirrorRenderer({
|
|
|
301
296
|
const convertedSegments = convertSegmentPositions(segments, content);
|
|
302
297
|
|
|
303
298
|
const segmentsRef = useRef(convertedSegments);
|
|
299
|
+
// Index segments by annotation ID for O(1) click lookups
|
|
300
|
+
const segmentsByIdRef = useRef(new Map<string, TextSegment>());
|
|
304
301
|
const lineNumbersCompartment = useRef(new Compartment());
|
|
305
302
|
const eventBusRef = useRef(eventBus);
|
|
306
303
|
const getTargetDocumentNameRef = useRef(getTargetDocumentName);
|
|
307
304
|
|
|
308
305
|
// Update refs when they change
|
|
309
306
|
segmentsRef.current = segments;
|
|
307
|
+
const segmentsById = new Map<string, TextSegment>();
|
|
308
|
+
for (const s of segments) {
|
|
309
|
+
if (s.annotation) segmentsById.set(s.annotation.id, s);
|
|
310
|
+
}
|
|
311
|
+
segmentsByIdRef.current = segmentsById;
|
|
310
312
|
eventBusRef.current = eventBus;
|
|
311
313
|
getTargetDocumentNameRef.current = getTargetDocumentName;
|
|
312
314
|
|
|
@@ -343,7 +345,7 @@ export function CodeMirrorRenderer({
|
|
|
343
345
|
const annotationId = annotationElement?.getAttribute('data-annotation-id');
|
|
344
346
|
|
|
345
347
|
if (annotationId && eventBusRef.current) {
|
|
346
|
-
const segment =
|
|
348
|
+
const segment = segmentsByIdRef.current.get(annotationId);
|
|
347
349
|
if (segment?.annotation) {
|
|
348
350
|
event.preventDefault();
|
|
349
351
|
eventBusRef.current.get('attend:click').next({
|
|
@@ -431,12 +433,67 @@ export function CodeMirrorRenderer({
|
|
|
431
433
|
if (annotationElement) handleMouseLeave();
|
|
432
434
|
};
|
|
433
435
|
|
|
436
|
+
// Delegated widget event handlers (replaces per-widget listeners)
|
|
437
|
+
const handleWidgetClick = (e: MouseEvent) => {
|
|
438
|
+
const target = e.target as HTMLElement;
|
|
439
|
+
const widget = target.closest('.reference-preview-widget') as HTMLElement | null;
|
|
440
|
+
if (!widget || widget.dataset.widgetGenerating === 'true') return;
|
|
441
|
+
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
e.stopPropagation();
|
|
444
|
+
|
|
445
|
+
const annotationId = widget.dataset.widgetAnnotationId;
|
|
446
|
+
const bodySource = widget.dataset.widgetBodySource;
|
|
447
|
+
const isResolved = widget.dataset.widgetResolved === 'true';
|
|
448
|
+
|
|
449
|
+
if (!annotationId || !eventBusRef.current) return;
|
|
450
|
+
|
|
451
|
+
if (isResolved && bodySource) {
|
|
452
|
+
eventBusRef.current.get('navigation:reference-navigate').next({ documentId: bodySource });
|
|
453
|
+
} else {
|
|
454
|
+
const motivation = (widget.dataset.widgetMotivation || 'linking') as Annotation['motivation'];
|
|
455
|
+
eventBusRef.current.get('attend:click').next({ annotationId, motivation });
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const handleWidgetMouseEnter = (e: MouseEvent) => {
|
|
460
|
+
const target = e.target as HTMLElement;
|
|
461
|
+
const widget = target.closest('.reference-preview-widget') as HTMLElement | null;
|
|
462
|
+
if (!widget || widget.dataset.widgetGenerating === 'true') return;
|
|
463
|
+
|
|
464
|
+
const indicator = widget.querySelector('.reference-indicator') as HTMLElement | null;
|
|
465
|
+
if (indicator) indicator.style.opacity = '1';
|
|
466
|
+
|
|
467
|
+
if (widget.dataset.widgetResolved === 'true' && widget.dataset.widgetTargetName) {
|
|
468
|
+
showWidgetPreview(widget, widget.dataset.widgetTargetName);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const handleWidgetMouseLeave = (e: MouseEvent) => {
|
|
473
|
+
const target = e.target as HTMLElement;
|
|
474
|
+
const widget = target.closest('.reference-preview-widget') as HTMLElement | null;
|
|
475
|
+
if (!widget) return;
|
|
476
|
+
|
|
477
|
+
const indicator = widget.querySelector('.reference-indicator') as HTMLElement | null;
|
|
478
|
+
if (indicator) indicator.style.opacity = '0.6';
|
|
479
|
+
|
|
480
|
+
if (widget.dataset.widgetResolved === 'true') {
|
|
481
|
+
hideWidgetPreview(widget);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
434
485
|
container.addEventListener('mouseover', handleMouseOver);
|
|
435
486
|
container.addEventListener('mouseout', handleMouseOut);
|
|
487
|
+
container.addEventListener('click', handleWidgetClick);
|
|
488
|
+
container.addEventListener('mouseenter', handleWidgetMouseEnter, true);
|
|
489
|
+
container.addEventListener('mouseleave', handleWidgetMouseLeave, true);
|
|
436
490
|
|
|
437
491
|
return () => {
|
|
438
492
|
container.removeEventListener('mouseover', handleMouseOver);
|
|
439
493
|
container.removeEventListener('mouseout', handleMouseOut);
|
|
494
|
+
container.removeEventListener('click', handleWidgetClick);
|
|
495
|
+
container.removeEventListener('mouseenter', handleWidgetMouseEnter, true);
|
|
496
|
+
container.removeEventListener('mouseleave', handleWidgetMouseLeave, true);
|
|
440
497
|
cleanupHover();
|
|
441
498
|
view.destroy();
|
|
442
499
|
viewRef.current = null;
|
|
@@ -496,7 +553,6 @@ export function CodeMirrorRenderer({
|
|
|
496
553
|
content,
|
|
497
554
|
segments: convertedSegments,
|
|
498
555
|
generatingReferenceId,
|
|
499
|
-
eventBus: eventBusRef.current,
|
|
500
556
|
getTargetDocumentName: getTargetDocumentNameRef.current
|
|
501
557
|
})
|
|
502
558
|
});
|
|
@@ -550,54 +606,6 @@ export function CodeMirrorRenderer({
|
|
|
550
606
|
};
|
|
551
607
|
}, [hoveredAnnotationId]);
|
|
552
608
|
|
|
553
|
-
// Handle hovered comment - add pulse effect and scroll if not visible
|
|
554
|
-
useEffect(() => {
|
|
555
|
-
if (!viewRef.current || !hoveredCommentId) return undefined;
|
|
556
|
-
|
|
557
|
-
const view = viewRef.current;
|
|
558
|
-
|
|
559
|
-
// Find the comment element in the DOM
|
|
560
|
-
const element = view.contentDOM.querySelector(
|
|
561
|
-
`[data-annotation-id="${CSS.escape(hoveredCommentId)}"]`
|
|
562
|
-
) as HTMLElement;
|
|
563
|
-
|
|
564
|
-
if (!element) return undefined;
|
|
565
|
-
|
|
566
|
-
// Find the actual scroll container - could be annotate view or document viewer
|
|
567
|
-
const scrollContainer = (element.closest('.semiont-annotate-view__content') ||
|
|
568
|
-
element.closest('.semiont-document-viewer__scrollable-body')) as HTMLElement;
|
|
569
|
-
|
|
570
|
-
if (scrollContainer) {
|
|
571
|
-
// Check visibility within the scroll container, not window
|
|
572
|
-
const elementRect = element.getBoundingClientRect();
|
|
573
|
-
const containerRect = scrollContainer.getBoundingClientRect();
|
|
574
|
-
|
|
575
|
-
const isVisible =
|
|
576
|
-
elementRect.top >= containerRect.top &&
|
|
577
|
-
elementRect.bottom <= containerRect.bottom;
|
|
578
|
-
|
|
579
|
-
if (!isVisible) {
|
|
580
|
-
// Manually scroll the container instead of using scrollIntoView
|
|
581
|
-
const elementTop = element.offsetTop;
|
|
582
|
-
const containerHeight = scrollContainer.clientHeight;
|
|
583
|
-
const elementHeight = element.offsetHeight;
|
|
584
|
-
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
585
|
-
|
|
586
|
-
scrollContainer.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Add pulse effect after a brief delay to ensure element is visible
|
|
591
|
-
const timeoutId = setTimeout(() => {
|
|
592
|
-
element.classList.add('annotation-pulse');
|
|
593
|
-
}, 100);
|
|
594
|
-
|
|
595
|
-
return () => {
|
|
596
|
-
clearTimeout(timeoutId);
|
|
597
|
-
element.classList.remove('annotation-pulse');
|
|
598
|
-
};
|
|
599
|
-
}, [hoveredCommentId]);
|
|
600
|
-
|
|
601
609
|
// Handle scroll to annotation
|
|
602
610
|
useEffect(() => {
|
|
603
611
|
if (!viewRef.current || !scrollToAnnotationId) return;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useRef, useEffect, useCallback, lazy, Suspense } from 'react';
|
|
4
4
|
import type { components } from '@semiont/core';
|
|
5
5
|
import { resourceUri as toResourceUri } from '@semiont/core';
|
|
6
|
-
import { getTextPositionSelector, getTextQuoteSelector, getTargetSelector, getMimeCategory, isPdfMimeType, extractContext, findTextWithContext } from '@semiont/api-client';
|
|
6
|
+
import { getTextPositionSelector, getTextQuoteSelector, getTargetSelector, getMimeCategory, isPdfMimeType, extractContext, findTextWithContext, buildContentCache } from '@semiont/api-client';
|
|
7
7
|
import { ANNOTATORS } from '../../lib/annotation-registry';
|
|
8
8
|
import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
9
9
|
import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
|
|
@@ -51,6 +51,9 @@ function segmentTextWithAnnotations(content: string, annotations: Annotation[]):
|
|
|
51
51
|
return [{ exact: '', start: 0, end: 0 }];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Pre-compute normalized/lowered content once for all annotations
|
|
55
|
+
const cache = buildContentCache(content);
|
|
56
|
+
|
|
54
57
|
const normalizedAnnotations = annotations
|
|
55
58
|
.map(ann => {
|
|
56
59
|
const targetSelector = getTargetSelector(ann.target);
|
|
@@ -66,7 +69,8 @@ function segmentTextWithAnnotations(content: string, annotations: Annotation[]):
|
|
|
66
69
|
quoteSelector.exact,
|
|
67
70
|
quoteSelector.prefix,
|
|
68
71
|
quoteSelector.suffix,
|
|
69
|
-
posSelector?.start
|
|
72
|
+
posSelector?.start,
|
|
73
|
+
cache
|
|
70
74
|
);
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -160,7 +164,7 @@ export function AnnotateView({
|
|
|
160
164
|
const segments = segmentTextWithAnnotations(content, allAnnotations);
|
|
161
165
|
|
|
162
166
|
// Extract UI state
|
|
163
|
-
const { selectedMotivation, selectedClick, selectedShape, hoveredAnnotationId,
|
|
167
|
+
const { selectedMotivation, selectedClick, selectedShape, hoveredAnnotationId, scrollToAnnotationId } = uiState;
|
|
164
168
|
|
|
165
169
|
// Store onUIStateChange in ref to avoid dependency issues
|
|
166
170
|
const onUIStateChangeRef = useRef(onUIStateChange);
|
|
@@ -336,7 +340,6 @@ export function AnnotateView({
|
|
|
336
340
|
editable={false}
|
|
337
341
|
newAnnotationIds={newAnnotationIds}
|
|
338
342
|
{...(hoveredAnnotationId !== undefined && { hoveredAnnotationId })}
|
|
339
|
-
{...(hoveredCommentId !== undefined && { hoveredCommentId })}
|
|
340
343
|
{...(scrollToAnnotationId !== undefined && { scrollToAnnotationId })}
|
|
341
344
|
sourceView={true}
|
|
342
345
|
showLineNumbers={showLineNumbers}
|
|
@@ -375,7 +378,7 @@ export function AnnotateView({
|
|
|
375
378
|
drawingMode={selectedMotivation ? selectedShape : null}
|
|
376
379
|
selectedMotivation={selectedMotivation}
|
|
377
380
|
eventBus={eventBus}
|
|
378
|
-
hoveredAnnotationId={
|
|
381
|
+
hoveredAnnotationId={hoveredAnnotationId || null}
|
|
379
382
|
hoverDelayMs={hoverDelayMs}
|
|
380
383
|
/>
|
|
381
384
|
</Suspense>
|
|
@@ -405,7 +408,7 @@ export function AnnotateView({
|
|
|
405
408
|
drawingMode={selectedMotivation ? selectedShape : null}
|
|
406
409
|
selectedMotivation={selectedMotivation}
|
|
407
410
|
eventBus={eventBus}
|
|
408
|
-
hoveredAnnotationId={
|
|
411
|
+
hoveredAnnotationId={hoveredAnnotationId || null}
|
|
409
412
|
hoverDelayMs={hoverDelayMs}
|
|
410
413
|
/>
|
|
411
414
|
)}
|
|
@@ -224,7 +224,6 @@ export function ResourceViewer({
|
|
|
224
224
|
// Internal UI state for hover, focus, and scroll
|
|
225
225
|
// Use prop value when provided (controlled by parent), otherwise null
|
|
226
226
|
const hoveredAnnotationId = hoveredAnnotationIdProp ?? null;
|
|
227
|
-
const [hoveredCommentId, _setHoveredCommentId] = useState<string | null>(null);
|
|
228
227
|
const [scrollToAnnotationId, setScrollToAnnotationId] = useState<string | null>(null);
|
|
229
228
|
const [_focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
230
229
|
|
|
@@ -413,7 +412,6 @@ export function ResourceViewer({
|
|
|
413
412
|
mimeType={mimeType}
|
|
414
413
|
resourceUri={resource['@id']}
|
|
415
414
|
annotations={annotationsCollection}
|
|
416
|
-
hoveredCommentId={hoveredCommentId}
|
|
417
415
|
selectedClick={selectedClick}
|
|
418
416
|
hoverDelayMs={hoverDelayMs}
|
|
419
417
|
annotateMode={annotateMode}
|