backnotprop / backnotprop/plannotator

Bug: empty/whitespace selection crashes _handleSelection and creates garbage annotations

Open
#881 0 comments 1 reaction 1 assignee Claimed by @backnotprop View on GitHub
Dominant language
TypeScript
Stars
8.7k
Forks
649
Avg merge
11h 12m
Merged PRs (30d)
109

Description

## Summary

When a user double-clicks at a block boundary (e.g. end of a heading element) or selects whitespace-only text, two bugs occur in `@plannotator/web-highlighter`'s `_handleSelection`:

### Bug 1 — `parentNode` crash (before CREATE event)

When the browser's selection range has a non-text `endContainer` that is an Element node with `endOffset >= childNodes.length` (pointing past the last child), `formatDomNode()` in `src/model/range/dom.ts` returns `{: undefined, offset: 0}`. This causes `getDomMeta → getOriginParent` to throw:

```
Cannot read properties of undefined (reading 'parentNode')
```

**Stack trace (approximate):**
`getOriginParent` ← `getDomMeta` ← `HighlightRange.serialize` ← `_highlightFromHRange` ← `_handleSelection`

**Root cause:** `formatDomNode` does not bounds-check the offset before accessing `childNodes[offset]`:

```ts
// src/model/range/dom.ts
return {
: n.$node.childNodes[n.offset], // undefined when offset >= childNodes.length
offset: 0,
};
```

**Minimal fix in `formatDomNode`:**
```ts
const child = n.$node.childNodes[n.offset];
if (!child) return n; // safe fallback
return { $node: child, offset: 0 };
```

### Bug 2 — Garbage annotation anchored to `"\n"` (CREATE fires)

When double-clicking in a whitespace region, the browser produces a non-collapsed range whose `selection.toString()` is only newlines or spaces. The CREATE event fires, and the resulting annotation has `originalText: "\n"`, which:
- Renders as an invisible highlight
- Can never be re-anchored on reload
- Gets saved to the annotation store

**Minimal fix in `getDomRange`:**
```ts
// src/model/range/selection.ts
if (selection.isCollapsed || !selection.toString().trim()) {
return null;
}
```

## Reproduction

1. Open a plan with a heading (e.g. `# Test Heading`)
2. Double-click at the very end of the heading text (just past the last character)
3. Observe console error: `Cannot read properties of undefined (reading 'parentNode')`

Or:

1. Open a plan with paragraph text
2. Double-click in the whitespace between two blocks
3. Observe a toolbar appearing with empty selection text; clicking an action creates a garbage annotation anchored to `"\n"`

## Affected version

`@plannotator/web-highlighter@0.8.1`

## Local workaround

A capture-phase `mouseup` guard on the container element that collapses the selection before web-highlighter's bubble-phase handler sees it.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.