Summary
A FreeText (and CalloutFreeText) annotation can end up with its typed text never committed to the annotation object — it stays only in the contentEditable DOM. exportAnnotations() and the onAnnotationEvent stream both miss it, so any host app that persists annotations will silently drop the text.
Affected versions
@embedpdf/plugin-annotation@2.14.4 (latest published)
- Still present on
main
Environment
- Using
@embedpdf/react-pdf-viewer, but the bug is in the shared component logic, not framework-specific.
Steps to reproduce
- Create a new FreeText annotation (enters edit mode).
- Open the "Free text styles" panel and change a style (e.g. background color). This blurs the editor and fires a style
update event.
- Click back into the same text box and type some text.
- Click/focus away from the box.
- No
update event fires, and the typed text is not in the annotation object — confirmed via exportAnnotations(), which still returns the old/empty contents.
You can watch the desync happen in the UI: the comment list in the right sidebar renders from the annotation object's contents, so as you type in step 3 it stops tracking the contentEditable and keeps showing the last-committed (stale/empty) text. The visible gap between the sidebar entry and what's in the text box is the internal state divergence made observable — no debug API needed.
Recovery that proves the diagnosis: double-click the annotation again (re-enters edit mode) and blur — now an update fires, the text is committed, and the sidebar entry snaps back into sync, because the editor's DOM still held the typed text.
Expected vs actual
- Expected: typing into a focused FreeText and blurring commits
contents and emits an update.
- Actual: if the editor was blurred once and re-focused without
isEditing changing, the blur is a no-op and the text is lost.
Summary
A FreeText (and CalloutFreeText) annotation can end up with its typed text never committed to the annotation object — it stays only in the contentEditable DOM.
exportAnnotations()and theonAnnotationEventstream both miss it, so any host app that persists annotations will silently drop the text.Affected versions
@embedpdf/plugin-annotation@2.14.4(latest published)mainEnvironment
@embedpdf/react-pdf-viewer, but the bug is in the shared component logic, not framework-specific.Steps to reproduce
updateevent.updateevent fires, and the typed text is not in the annotation object — confirmed viaexportAnnotations(), which still returns the old/emptycontents.You can watch the desync happen in the UI: the comment list in the right sidebar renders from the annotation object's
contents, so as you type in step 3 it stops tracking the contentEditable and keeps showing the last-committed (stale/empty) text. The visible gap between the sidebar entry and what's in the text box is the internal state divergence made observable — no debug API needed.Recovery that proves the diagnosis: double-click the annotation again (re-enters edit mode) and blur — now an
updatefires, the text is committed, and the sidebar entry snaps back into sync, because the editor's DOM still held the typed text.Expected vs actual
contentsand emits anupdate.isEditingchanging, the blur is a no-op and the text is lost.