Fix emoji bug on the demo website

This commit is contained in:
Andras Schmelczer 2026-10-27 20:53:36 +00:00
commit 55e8177255
3 changed files with 37 additions and 10 deletions

View file

@ -128,6 +128,24 @@ console.log(result.cursors); // [{ id: 2, position: 0 }, { id: 1, position: 3 }]
> The `cursors` list is sorted by character position (not IDs).
Input and output cursor positions count Unicode scalars (code points), starting
at zero and allowing a position just after the last scalar. They do not count
UTF-16 code units or grapheme clusters. For example, the position after `😀` is
`1`, although JavaScript's `'😀'.length` is `2`.
DOM `selectionStart` and `selectionEnd` use UTF-16 code units. Convert both
endpoints before passing a textarea selection to the library:
```typescript
const edited = {
text: textarea.value,
cursors: [
{ id: 1, position: [...textarea.value.slice(0, textarea.selectionStart)].length },
{ id: 2, position: [...textarea.value.slice(0, textarea.selectionEnd)].length },
],
} satisfies TextWithOptionalCursors;
```
## Generic Helpers and Inference
The exported merge functions are intentionally small: they merge strings, or

View file

@ -1,5 +1,5 @@
import { reconcileWithHistory } from 'reconcile-text';
import type { BuiltinTokenizer } from 'reconcile-text';
import type { BuiltinTokenizer, CursorPosition } from 'reconcile-text';
import './style.scss';
const originalTextArea = document.getElementById('original') as HTMLTextAreaElement;
@ -157,19 +157,22 @@ function getCursorsFromActiveTextArea() {
let rightCursors = undefined;
if (activeElement === leftTextArea) {
leftCursors = [
{ id: 1, position: leftTextArea.selectionStart },
{ id: 2, position: leftTextArea.selectionEnd },
];
leftCursors = getSelectionCursors(leftTextArea);
} else if (activeElement === rightTextArea) {
rightCursors = [
{ id: 1, position: rightTextArea.selectionStart },
{ id: 2, position: rightTextArea.selectionEnd },
];
rightCursors = getSelectionCursors(rightTextArea);
}
return { leftCursors, rightCursors };
}
function getSelectionCursors(textarea: HTMLTextAreaElement): CursorPosition[] {
const text = textarea.value;
// DOM selections count UTF-16 code units; reconcile counts Unicode scalars.
return [
{ id: 1, position: [...text.slice(0, textarea.selectionStart)].length },
{ id: 2, position: [...text.slice(0, textarea.selectionEnd)].length },
];
}
function createSelectionOverlay(isLeft: boolean, isSelection: boolean): HTMLSpanElement {
const caretSpan = document.createElement('span');
caretSpan.className = `selection-caret selection-caret-${isLeft ? 'left' : 'right'}`;

View file

@ -136,7 +136,13 @@ export interface CursorPosition {
/** Unique identifier for the cursor (can be any number, must be unique within the document) */
id: number;
/** Character position in the text, 0-based index from the beginning of the document */
/**
* Zero-based offset in Unicode scalars (code points), from 0 through
* `[...text].length` inclusive. Applies to both input and output cursors.
* These are not UTF-16 code units or grapheme clusters: `😀` counts as one.
* Convert DOM selectionStart/selectionEnd offsets before passing them in:
* `[...text.slice(0, offset)].length`.
*/
position: number;
}