Skip to content

Commit a1d214e

Browse files
committed
Add edit-after-create support for annotations
Introduce editAfterCreate behavior across the annotation plugin. This adds a new editAfterCreate option on tools and plugin config, threads it through AnnotationPlugin.createAnnotation (new options param) and AnnotationEvent, and uses it on commit to select and/or start editing newly created annotations. UI changes across React/Svelte/Vue components: allow pointer events when an annotation is being edited, prevent background deselects while entering edit mode, and set editingId from create events. FreeText editor: avoid collapsing selection when the content equals the tool default and normalize non-breaking spaces to regular spaces on update. Also enable editAfterCreate in the default free-text tool and update reducer defaults to include editAfterCreate.
1 parent a55afcb commit a1d214e

14 files changed

Lines changed: 94 additions & 26 deletions

File tree

packages/plugin-annotation/src/lib/annotation-plugin.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,11 +611,12 @@ export class AnnotationPlugin extends BasePlugin<
611611
services: callbacks.services, // Pass through services
612612
onPreview: (state) => callbacks.onPreview(tool.id, state),
613613
onCommit: (annotation, ctx) => {
614-
this.createAnnotation(pageIndex, annotation, ctx, documentId);
614+
const editAfterCreate = tool.behavior?.editAfterCreate ?? false;
615+
this.createAnnotation(pageIndex, annotation, ctx, documentId, { editAfterCreate });
615616
if (tool.behavior?.deactivateToolAfterCreate) {
616617
this.setActiveTool(null, documentId);
617618
}
618-
if (tool.behavior?.selectAfterCreate) {
619+
if (tool.behavior?.selectAfterCreate || editAfterCreate) {
619620
this.selectAnnotation(pageIndex, annotation.id, documentId);
620621
}
621622
},
@@ -854,6 +855,7 @@ export class AnnotationPlugin extends BasePlugin<
854855
annotation: A,
855856
ctx?: AnnotationCreateContext<A>,
856857
documentId?: string,
858+
options?: { editAfterCreate?: boolean },
857859
) {
858860
const docId = documentId ?? this.getActiveDocumentId();
859861

@@ -875,6 +877,7 @@ export class AnnotationPlugin extends BasePlugin<
875877
...annotation,
876878
author: annotation.author ?? this.config.annotationAuthor,
877879
};
880+
const editAfterCreate = options?.editAfterCreate;
878881
const execute = () => {
879882
this.dispatch(createAnnotation(docId, pageIndex, newAnnotation));
880883
if (ctx) contexts.set(id, ctx);
@@ -885,6 +888,7 @@ export class AnnotationPlugin extends BasePlugin<
885888
pageIndex,
886889
ctx,
887890
committed: false,
891+
editAfterCreate,
888892
});
889893
};
890894

packages/plugin-annotation/src/lib/reducer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export const initialState = (cfg: AnnotationPluginConfig): AnnotationState => {
148148
deactivateToolAfterCreate:
149149
t.behavior?.deactivateToolAfterCreate ?? cfg.deactivateToolAfterCreate ?? false,
150150
selectAfterCreate: t.behavior?.selectAfterCreate ?? cfg.selectAfterCreate ?? true,
151+
editAfterCreate: t.behavior?.editAfterCreate ?? cfg.editAfterCreate ?? false,
151152
},
152153
}));
153154

packages/plugin-annotation/src/lib/tools/default-tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,8 @@ export const defaultTools = [
439439
},
440440
behavior: {
441441
insertUpright: true,
442+
editAfterCreate: true,
443+
selectAfterCreate: true,
442444
},
443445
},
444446
{

packages/plugin-annotation/src/lib/tools/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ export type AnnotationTool<T extends PdfAnnotationObject = PdfAnnotationObject>
210210
deactivateToolAfterCreate?: boolean;
211211
/** When true, select the annotation immediately after creation. Overrides plugin config. */
212212
selectAfterCreate?: boolean;
213+
/** When true, automatically enter editing mode after creating the annotation. Implies selectAfterCreate. */
214+
editAfterCreate?: boolean;
213215
/** Override whether this annotation type uses AP rendering before editing (default: true) */
214216
useAppearanceStream?: boolean;
215217
} & InsertUprightBehaviorFor<T> &

packages/plugin-annotation/src/lib/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type AnnotationEvent =
3232
pageIndex: number;
3333
ctx?: AnnotationCreateContext<any>;
3434
committed: boolean;
35+
editAfterCreate?: boolean;
3536
}
3637
| {
3738
type: 'update';
@@ -148,6 +149,8 @@ export interface AnnotationPluginConfig extends BasePluginConfig {
148149
deactivateToolAfterCreate?: boolean;
149150
/** When true (default false), select the annotation immediately after creation. */
150151
selectAfterCreate?: boolean;
152+
/** When true (default false), automatically enter edit mode after creating an annotation. */
153+
editAfterCreate?: boolean;
151154
}
152155

153156
/**

packages/plugin-annotation/src/shared/components/annotation-container.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ export function AnnotationContainer<T extends PdfAnnotationObject>({
491491
}}
492492
>
493493
{/* Inner div: rotated content — visual only, no drag/interaction */}
494-
<div style={{ ...innerDivBaseStyle, pointerEvents: 'none' }}>
494+
<div style={{ ...innerDivBaseStyle, pointerEvents: isEditing ? 'auto' : 'none' }}>
495495
{/* Dict content -- always in DOM so hit area handles clicks */}
496496
{(() => {
497497
const childrenRender =
@@ -671,7 +671,7 @@ export function AnnotationContainer<T extends PdfAnnotationObject>({
671671
...innerDivBaseStyle,
672672
outline: showOutline ? `${outlineWidth}px ${outlineStyle} ${outlineColor}` : 'none',
673673
outlineOffset: showOutline ? `${outlineOff}px` : '0px',
674-
pointerEvents: isSelected && !isMultiSelected ? 'auto' : 'none',
674+
pointerEvents: isSelected && !isMultiSelected && !isEditing ? 'auto' : 'none',
675675
touchAction: 'none',
676676
cursor: isSelected && effectiveIsDraggable ? 'move' : 'default',
677677
}}

packages/plugin-annotation/src/shared/components/annotations.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ export function Annotations(annotationsProps: AnnotationsProps) {
8383
}
8484
}, [annotationProvides, pageIndex]);
8585

86+
useEffect(() => {
87+
if (!annotationProvides) return;
88+
return annotationProvides.onAnnotationEvent((event) => {
89+
if (event.type === 'create' && event.editAfterCreate) {
90+
setEditingId(event.annotation.id);
91+
}
92+
});
93+
}, [annotationProvides]);
94+
8695
useEffect(() => {
8796
if (!annotationProvides) return;
8897

@@ -105,12 +114,15 @@ export function Annotations(annotationsProps: AnnotationsProps) {
105114
(): PointerEventHandlers<EmbedPdfPointerEvent<MouseEvent>> => ({
106115
onPointerDown: (_, pe) => {
107116
if (pe.target === pe.currentTarget && annotationProvides) {
117+
if (editingId && annotations.some((a) => a.object.id === editingId)) {
118+
pe.stopImmediatePropagation();
119+
}
108120
annotationProvides.deselectAnnotation();
109121
setEditingId(null);
110122
}
111123
},
112124
}),
113-
[annotationProvides],
125+
[annotationProvides, editingId, annotations],
114126
);
115127

116128
const handleClick = useCallback(

packages/plugin-annotation/src/shared/components/annotations/free-text.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,17 @@ export function FreeText({
5050
const editor = editorRef.current;
5151
editor.focus();
5252

53+
const tool = annotationProvides?.findToolForAnnotation(annotation.object);
54+
const isDefaultContent =
55+
tool?.defaults?.contents != null && annotation.object.contents === tool.defaults.contents;
56+
5357
const selection = window.getSelection();
5458
if (selection) {
5559
const range = document.createRange();
5660
range.selectNodeContents(editor);
57-
range.collapse(false);
61+
if (!isDefaultContent) {
62+
range.collapse(false);
63+
}
5864
selection.removeAllRanges();
5965
selection.addRange(range);
6066
}
@@ -79,7 +85,7 @@ export function FreeText({
7985
if (!annotationProvides) return;
8086
if (!editorRef.current) return;
8187
annotationProvides.updateAnnotation(pageIndex, annotation.object.id, {
82-
contents: editorRef.current.innerText,
88+
contents: editorRef.current.innerText.replace(/\u00A0/g, ' '),
8389
});
8490
};
8591

packages/plugin-annotation/src/svelte/components/AnnotationContainer.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@
464464
style:height="{innerHeight}px"
465465
style:transform={annotationRotation !== 0 ? `rotate(${annotationRotation}deg)` : undefined}
466466
style:transform-origin={innerTransformOrigin}
467-
style:pointer-events="none"
467+
style:pointer-events={isEditing ? 'auto' : 'none'}
468468
>
469469
<!-- Annotation content - renders in unrotated coordinate space -->
470470
{#if customAnnotationRenderer}
@@ -639,7 +639,7 @@
639639
style:transform-origin={innerTransformOrigin}
640640
style:outline={showOutline ? `${outlineWidth}px ${outlineStyleVal} ${outlineColor}` : 'none'}
641641
style:outline-offset={showOutline ? `${outlineOff}px` : '0px'}
642-
style:pointer-events={isSelected && !isMultiSelected ? 'auto' : 'none'}
642+
style:pointer-events={isSelected && !isMultiSelected && !isEditing ? 'auto' : 'none'}
643643
style:touch-action="none"
644644
style:cursor={isSelected && effectiveIsDraggable ? 'move' : 'default'}
645645
>

packages/plugin-annotation/src/svelte/components/Annotations.svelte

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@
125125
return () => off?.();
126126
});
127127
128+
$effect(() => {
129+
if (!annotationProvides) return;
130+
return annotationProvides.onAnnotationEvent((event) => {
131+
if (event.type === 'create' && event.editAfterCreate) {
132+
editingId = event.annotation.id;
133+
}
134+
});
135+
});
136+
128137
// Fetch appearance map, invalidate on scale change
129138
$effect(() => {
130139
if (!annotationProvides) return;
@@ -148,6 +157,9 @@
148157
const handlers: PointerEventHandlersWithLifecycle<EmbedPdfPointerEvent<PointerEvent>> = {
149158
onPointerDown: (_, pe) => {
150159
if (pe.target === pe.currentTarget && annotationProvides) {
160+
if (editingId && annotations.some((a) => a.object.id === editingId)) {
161+
pe.stopImmediatePropagation();
162+
}
151163
annotationProvides.deselectAnnotation();
152164
editingId = null;
153165
}

0 commit comments

Comments
 (0)