A comment in a document is attached to a piece of text. A reviewer highlights a sentence, writes “cut this”, and the writer expects the note to stay on that sentence. Then someone edits the paragraph above it, deletes the highlighted words, or pastes a new section in the middle. The comment has to go somewhere. An anchor that is stored as a character offset drifts out of place with every edit before it. An anchor that is stored as a pixel position is worse. The design decision is how the anchor follows the text as it changes.
Character offsets break on the first edit
The simplest anchor is a pair of numbers: the comment covers characters 1,204 to 1,231. A writer who adds a word at the start of the document moves everything after it by a few characters, and the anchor now covers the wrong words. Offsets are fine for a snapshot of a document that never changes, and wrong for a document that does. Store the anchor as something that moves with the content.
Map anchors through each edit
Editors that use a transaction model can map a position through a change. Each edit describes which ranges were inserted, deleted or replaced. A position before an insertion moves forward by the inserted length. A position inside a deleted range collapses to the edge of the deletion. Apply that mapping to every stored anchor when a change is committed, and the anchor follows the text it was attached to.
type Anchor = { from: number; to: number; commentId: string };
// Map one position through a replacement of [start, end) with `length` new characters.
function mapPos(pos: number, start: number, end: number, length: number): number {
if (pos <= start) return pos;
if (pos >= end) return pos - (end - start) + length;
return start; // the position was inside the replaced text: move it to the edge
}
function mapAnchor(a: Anchor, start: number, end: number, length: number): Anchor {
return { ...a, from: mapPos(a.from, start, end, length), to: mapPos(a.to, start, end, length) };
}The sample handles one replacement. A real editor applies a list of them in order. The rule about positions inside the replaced range matters most. Collapsing to the start is a choice, and a comment anchor that collapses to zero width should be shown as a marker, not deleted, so the note is not lost.
A deleted anchor is not a deleted comment
When the writer deletes the sentence a comment is attached to, the comment should survive. Reviewers want to see that the text they discussed was removed, and sometimes they want to read the discussion before the change was made. Keep the comment, mark its anchor as orphaned, and show it in a list the writer can open. Deleting the comment on the same keystroke as the text it marks is a common way to lose a thread of review.
Collaborators make anchors harder
When two people edit at once, each edit arrives in a different order on each screen. The anchor must be mapped through the edits in a order that every client agrees on, or two readers will see the comment on different words. Store anchors inside the same shared model as the content, so the collaboration layer maps them with the text. A separate store that updates later will disagree with the document for a short time, and that is enough to confuse a review.
Exports need a plan for comments
A PDF or a printed copy has no margin for comments unless the export puts them there. Decide whether comments are exported as footnotes, as margin notes, or left out, and say which in the export dialog. Whatever the choice, the export must not show a comment on the wrong sentence, because a reader will assume it belongs to the words beside it.
A short test list
- Add a comment to a sentence, then insert a paragraph above it. The comment should stay on the same sentence.
- Delete the commented sentence. The comment should remain, marked as orphaned.
- Select part of a commented sentence and replace it. The comment should cover the new text, not vanish.
- Have two users comment and edit at the same time. Both screens should show each comment on the same words.
- Export with comments and check that each note appears next to the text it was attached to.
RichTextEditor is a perpetual-licence JavaScript editor — one purchase, self-hosted, no metered editor loads. Download the evaluation or see how it compares.