Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/backspace-delete-annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@embedpdf/plugin-annotation': minor
---

Add Backspace/Delete keyboard support for deleting selected annotations.

- New `deleteSelectedAnnotations()` method on the annotation capability and document scope. It deletes every currently selected annotation (shapes, free text, ink/signatures, stamps, form-field widgets, etc.) in one call, respecting the existing `ModifyAnnotations` permission guard and undo/redo history.
- The default viewer's `annotation:delete-selected` command is now bound to the `Backspace` and `Delete` keys and deletes the full selection (previously only the first selected annotation). When no annotation is selected it falls back to removing a selected pending redaction, so the same keys also delete redactions. The command is disabled when nothing is selected, so the keys keep their native behaviour otherwise. Typing in form fields / free-text editors is unaffected (the shortcut handler ignores key presses inside inputs and editable content).
39 changes: 27 additions & 12 deletions examples/react-tailwind/src/config/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,20 +998,35 @@ export const commands: Record<string, Command<State>> = {
id: 'annotation:delete-selected',
labelKey: 'annotation.deleteSelected',
icon: 'Trash',
shortcuts: ['Backspace', 'Delete'],
categories: ['annotation'],
action: ({ registry, documentId }) => {
const annotation = registry.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)?.provides();

const annotationScope = annotation?.forDocument(documentId);
if (!annotationScope) return;

const selectedAnnotation = annotationScope.getSelectedAnnotation();
if (!selectedAnnotation) return;

annotationScope.deleteAnnotation(
selectedAnnotation.object.pageIndex,
selectedAnnotation.object.id,
);
// Delete the active selection. Annotations (shapes, free text, ink,
// stamps, form widgets, ...) take priority; if none are selected, fall
// back to a selected pending redaction (redaction has its own selection).
const annotationScope = registry
.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
if (annotationScope && annotationScope.getSelectedAnnotations().length > 0) {
annotationScope.deleteSelectedAnnotations();
return;
}
const redactionScope = registry
.getPlugin<RedactionPlugin>(REDACTION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
const selectedRedaction = redactionScope?.getSelectedPending();
if (selectedRedaction) {
redactionScope?.removePending(selectedRedaction.page, selectedRedaction.id);
}
},
disabled: ({ state, documentId }) => {
const annotationSelected =
(state.plugins[ANNOTATION_PLUGIN_ID]?.documents[documentId]?.selectedUids?.length ?? 0) > 0;
const redactionSelected =
!!state.plugins[REDACTION_PLUGIN_ID]?.documents[documentId]?.selected;
return !annotationSelected && !redactionSelected;
},
},

Expand Down
41 changes: 28 additions & 13 deletions examples/svelte-tailwind/src/lib/config/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1194,23 +1194,38 @@ export const commands: Record<string, Command<State>> = {
id: 'annotation:delete-selected',
labelKey: 'annotation.deleteSelected',
icon: 'trash',
shortcuts: ['Backspace', 'Delete'],
categories: ['annotation'],
action: ({ registry, documentId }) => {
const annotation = registry.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)?.provides();

const annotationScope = annotation?.forDocument(documentId);
if (!annotationScope) return;

const selectedAnnotation = annotationScope.getSelectedAnnotation();
if (!selectedAnnotation) return;

annotationScope.deleteAnnotation(
selectedAnnotation.object.pageIndex,
selectedAnnotation.object.id,
);
// Delete the active selection. Annotations (shapes, free text, ink,
// stamps, form widgets, ...) take priority; if none are selected, fall
// back to a selected pending redaction (redaction has its own selection).
const annotationScope = registry
.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
if (annotationScope && annotationScope.getSelectedAnnotations().length > 0) {
annotationScope.deleteSelectedAnnotations();
return;
}
const redactionScope = registry
.getPlugin<RedactionPlugin>(REDACTION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
const selectedRedaction = redactionScope?.getSelectedPending();
if (selectedRedaction) {
redactionScope?.removePending(selectedRedaction.page, selectedRedaction.id);
}
},
disabled: ({ state, documentId }) => {
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
const annotationSelected =
(state.plugins[ANNOTATION_PLUGIN_ID]?.documents[documentId]?.selectedUids?.length ?? 0) > 0;
if (annotationSelected) {
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
}
const redactionSelected =
!!state.plugins[REDACTION_PLUGIN_ID]?.documents[documentId]?.selected;
return !redactionSelected;
},
},

Expand Down
41 changes: 28 additions & 13 deletions examples/vue-tailwind/src/config/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1194,23 +1194,38 @@ export const commands: Record<string, Command<State>> = {
id: 'annotation:delete-selected',
labelKey: 'annotation.deleteSelected',
icon: 'Trash',
shortcuts: ['Backspace', 'Delete'],
categories: ['annotation'],
action: ({ registry, documentId }) => {
const annotation = registry.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)?.provides();

const annotationScope = annotation?.forDocument(documentId);
if (!annotationScope) return;

const selectedAnnotation = annotationScope.getSelectedAnnotation();
if (!selectedAnnotation) return;

annotationScope.deleteAnnotation(
selectedAnnotation.object.pageIndex,
selectedAnnotation.object.id,
);
// Delete the active selection. Annotations (shapes, free text, ink,
// stamps, form widgets, ...) take priority; if none are selected, fall
// back to a selected pending redaction (redaction has its own selection).
const annotationScope = registry
.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
if (annotationScope && annotationScope.getSelectedAnnotations().length > 0) {
annotationScope.deleteSelectedAnnotations();
return;
}
const redactionScope = registry
.getPlugin<RedactionPlugin>(REDACTION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
const selectedRedaction = redactionScope?.getSelectedPending();
if (selectedRedaction) {
redactionScope?.removePending(selectedRedaction.page, selectedRedaction.id);
}
},
disabled: ({ state, documentId }) => {
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
const annotationSelected =
(state.plugins[ANNOTATION_PLUGIN_ID]?.documents[documentId]?.selectedUids?.length ?? 0) > 0;
if (annotationSelected) {
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
}
const redactionSelected =
!!state.plugins[REDACTION_PLUGIN_ID]?.documents[documentId]?.selected;
return !redactionSelected;
},
},

Expand Down
13 changes: 13 additions & 0 deletions packages/plugin-annotation/src/lib/annotation-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ export class AnnotationPlugin extends BasePlugin<
deleteAnnotation: (pageIndex, id) => this.deleteAnnotation(pageIndex, id),
deleteAnnotations: (annotations, documentId) =>
this.deleteAnnotationsMethod(annotations, documentId),
deleteSelectedAnnotations: (documentId) => this.deleteSelectedAnnotationsMethod(documentId),
deleteAllAnnotations: (documentId) => this.deleteAllAnnotationsMethod(documentId),
purgeAnnotation: (pageIndex, id, documentId) =>
this.purgeAnnotationMethod(pageIndex, id, documentId),
Expand Down Expand Up @@ -538,6 +539,7 @@ export class AnnotationPlugin extends BasePlugin<
this.moveAnnotationMethod(pageIndex, id, position, mode, documentId),
deleteAnnotation: (pageIndex, id) => this.deleteAnnotation(pageIndex, id, documentId),
deleteAnnotations: (annotations) => this.deleteAnnotationsMethod(annotations, documentId),
deleteSelectedAnnotations: () => this.deleteSelectedAnnotationsMethod(documentId),
deleteAllAnnotations: () => this.deleteAllAnnotationsMethod(documentId),
purgeAnnotation: (pageIndex, id) => this.purgeAnnotationMethod(pageIndex, id, documentId),
renderAnnotation: (options) => this.renderAnnotation(options, documentId),
Expand Down Expand Up @@ -1252,6 +1254,17 @@ export class AnnotationPlugin extends BasePlugin<
}
}

private deleteSelectedAnnotationsMethod(documentId?: string): void {
const docId = documentId ?? this.getActiveDocumentId();
const selected = this.getSelectedAnnotationsMethod(docId);
if (selected.length === 0) return;
const toDelete = selected.map((ta) => ({
pageIndex: ta.object.pageIndex,
id: ta.object.id,
}));
this.deleteAnnotationsMethod(toDelete, docId);
}

private purgeAnnotationMethod(pageIndex: number, id: string, documentId?: string): void {
const docId = documentId ?? this.getActiveDocumentId();
this.dispatch(purgeAnnotation(docId, pageIndex, id));
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-annotation/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ export interface AnnotationScope<TTools extends AnnotationToolMap = AnnotationTo
deleteAnnotation(pageIndex: number, annotationId: string): void;
/** Delete multiple annotations in batch */
deleteAnnotations(annotations: Array<{ pageIndex: number; id: string }>): void;
/** Delete every currently selected annotation (no-op if the selection is empty) */
deleteSelectedAnnotations(): void;
/** Delete all annotations from the document */
deleteAllAnnotations(): void;
/** Remove an annotation from state without calling the engine (no PDF modification) */
Expand Down Expand Up @@ -477,6 +479,8 @@ export interface AnnotationCapability<TTools extends AnnotationToolMap = Annotat
annotations: Array<{ pageIndex: number; id: string }>,
documentId?: string,
) => void;
/** Delete every currently selected annotation (no-op if the selection is empty) */
deleteSelectedAnnotations: (documentId?: string) => void;
/** Delete all annotations from the document */
deleteAllAnnotations: (documentId?: string) => void;
/** Remove an annotation from state without calling the engine (no PDF modification) */
Expand Down
41 changes: 28 additions & 13 deletions viewers/snippet/src/config/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1889,23 +1889,38 @@ export const commands: Record<string, Command<State>> = {
id: 'annotation:delete-selected',
labelKey: 'annotation.deleteSelected',
icon: 'trash',
shortcuts: ['Backspace', 'Delete'],
categories: ['annotation', 'annotation-delete'],
action: ({ registry, documentId }) => {
const annotation = registry.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)?.provides();

const annotationScope = annotation?.forDocument(documentId);
if (!annotationScope) return;

const selectedAnnotation = annotationScope.getSelectedAnnotation();
if (!selectedAnnotation) return;

annotationScope.deleteAnnotation(
selectedAnnotation.object.pageIndex,
selectedAnnotation.object.id,
);
// Delete the active selection. Annotations (shapes, free text, ink,
// stamps, form widgets, ...) take priority; if none are selected, fall
// back to a selected pending redaction (redaction has its own selection).
const annotationScope = registry
.getPlugin<AnnotationPlugin>(ANNOTATION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
if (annotationScope && annotationScope.getSelectedAnnotations().length > 0) {
annotationScope.deleteSelectedAnnotations();
return;
}
const redactionScope = registry
.getPlugin<RedactionPlugin>(REDACTION_PLUGIN_ID)
?.provides()
?.forDocument(documentId);
const selectedRedaction = redactionScope?.getSelectedPending();
if (selectedRedaction) {
redactionScope?.removePending(selectedRedaction.page, selectedRedaction.id);
}
},
disabled: ({ state, documentId }) => {
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
const annotationSelected =
(state.plugins[ANNOTATION_PLUGIN_ID]?.documents[documentId]?.selectedUids?.length ?? 0) > 0;
if (annotationSelected) {
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
}
const redactionSelected =
!!state.plugins[REDACTION_PLUGIN_ID]?.documents[documentId]?.selected;
return !redactionSelected;
},
},

Expand Down