Skip to content

Commit c6a5b65

Browse files
committed
fix(web): handle text input metrics correctly on history undo events
1 parent ae487c7 commit c6a5b65

File tree

2 files changed

+78
-31
lines changed

2 files changed

+78
-31
lines changed

src/MarkdownTextInput.web.tsx

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,6 @@ interface MarkdownNativeEvent extends Event {
6565
inputType: string;
6666
}
6767

68-
type Selection = {
69-
start: number;
70-
end: number;
71-
};
72-
7368
type Dimensions = {
7469
width: number;
7570
height: number;
@@ -179,7 +174,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
179174
const pasteRef = useRef<boolean>(false);
180175
const divRef = useRef<HTMLDivElement | null>(null);
181176
const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
182-
const contentSelection = useRef<Selection | null>(null);
177+
const contentSelection = useRef<ParseUtils.Selection | null>(null);
183178
const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
184179
const history = useRef<InputHistory>();
185180
const dimensions = React.useRef<Dimensions | null>(null);
@@ -303,15 +298,15 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
303298
[onSelectionChange, setEventProps],
304299
);
305300

306-
const updateRefSelectionVariables = useCallback((newSelection: Selection) => {
301+
const updateRefSelectionVariables = useCallback((newSelection: ParseUtils.Selection) => {
307302
const {start, end} = newSelection;
308303
const markdownHTMLInput = divRef.current as HTMLInputElement;
309304
markdownHTMLInput.selectionStart = start;
310305
markdownHTMLInput.selectionEnd = end;
311306
}, []);
312307

313308
const updateSelection = useCallback(
314-
(e: SyntheticEvent<HTMLDivElement> | null = null, predefinedSelection: Selection | null = null) => {
309+
(e: SyntheticEvent<HTMLDivElement> | null = null, predefinedSelection: ParseUtils.Selection | null = null) => {
315310
if (!divRef.current) {
316311
return;
317312
}
@@ -400,26 +395,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
400395
}>;
401396
setEventProps(event);
402397

403-
// The new text is between the prev start selection and the new end selection, can be empty
404-
const addedText = normalizedText.slice(prevSelection.start, cursorPosition ?? 0);
405-
// The length of the text that replaced the before text
406-
const count = addedText.length;
407-
// The start index of the replacement operation
408-
let start = prevSelection.start;
409-
410-
const prevSelectionRange = prevSelection.end - prevSelection.start;
411-
// The length the deleted text had before
412-
let before = prevSelectionRange;
413-
if (prevSelectionRange === 0 && (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward')) {
414-
// its possible the user pressed a delete key without a selection range, so we need to adjust the before value to have the length of the deleted text
415-
before = prevTextLength - normalizedText.length;
416-
}
417-
418-
if (inputType === 'deleteContentBackward') {
419-
// When the user does a backspace delete he expects the content before the cursor to be removed.
420-
// For this the start value needs to be adjusted (its as if the selection was before the text that we want to delete)
421-
start -= before;
422-
}
398+
const {start, before, count} = ParseUtils.calculateInputMetrics(inputType, prevSelection, prevTextLength, normalizedText, cursorPosition);
423399

424400
event.nativeEvent.count = count;
425401
event.nativeEvent.before = before;
@@ -660,7 +636,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
660636
return;
661637
}
662638

663-
const newSelection: Selection = {start: selection.start, end: selection.end ?? selection.start};
639+
const newSelection: ParseUtils.Selection = {start: selection.start, end: selection.end ?? selection.start};
664640
contentSelection.current = newSelection;
665641
updateRefSelectionVariables(newSelection);
666642
CursorUtils.setCursorPosition(divRef.current, newSelection.start, newSelection.end);

src/web/parserUtils.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ type NestedNode = {
1818
endIndex: number;
1919
};
2020

21+
type TextChangeMetrics = {
22+
/**
23+
* The start index in the provided string where the repalcement started from.
24+
*/
25+
start: number;
26+
/**
27+
* The amount of characters that have been added.
28+
*/
29+
count: number;
30+
/**
31+
* The amount of characters replaced.
32+
*/
33+
before: number;
34+
};
35+
36+
type Selection = {
37+
start: number;
38+
end: number;
39+
};
40+
2141
function addStyling(targetElement: HTMLElement, type: MarkdownType, markdownStyle: PartialMarkdownStyle) {
2242
const node = targetElement;
2343
switch (type) {
@@ -223,6 +243,57 @@ function parseText(target: HTMLElement, text: string, cursorPositionIndex: numbe
223243
return {text: target.innerText, cursorPosition: cursorPosition || 0};
224244
}
225245

226-
export {parseText, parseRangesToHTMLNodes};
246+
/**
247+
* Calculates start, count and before values. Whenever the text is being changed you can think of it as a replacement operation,
248+
* where parts of the string get replaced with new content.
249+
* For example:
250+
*
251+
* 1. Text input is abc
252+
* 2. User adds "d"
253+
* 3. start: 3, before: 0, count: 1
254+
*
255+
* We started to replace text starting from position 3, we replaced 0 characters and added 1 character.
256+
* Another example:
257+
*
258+
* 1. Text input is abc
259+
* 2. User selects "b" and adds "d"
260+
* 3. start: 1, before: 1, count: 1
261+
*
262+
* We started to replace text starting from position 1, we replaced 1 character and added 1 character.
263+
*
264+
* This is to align the onChange event with the native counter part:
265+
* - https://github.com/facebook/react-native/pull/45248
266+
*/
267+
function calculateInputMetrics(inputType: string, prevSelection: Selection, prevTextLength: number, normalizedText: string, cursorPosition: number | null): TextChangeMetrics {
268+
// The new text is between the prev start selection and the new end selection, can be empty
269+
const addedText = normalizedText.slice(prevSelection.start, cursorPosition ?? 0);
270+
// The length of the text that replaced the "before" text
271+
const count = addedText.length;
272+
// The start index of the replacement operation
273+
let start = prevSelection.start;
274+
// Before is by default the length of the previous selection
275+
let before = prevSelection.end - prevSelection.start;
276+
277+
// For some events start and before need to be adjusted
278+
if (inputType === 'historyUndo') {
279+
start = cursorPosition ?? 0;
280+
before = prevTextLength - normalizedText.length;
281+
} else if (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward') {
282+
if (before === 0) {
283+
// Its possible the user pressed a delete key without a selection range (before = 0),
284+
// so we need to adjust the before value to have the length of the deleted text
285+
before = prevTextLength - normalizedText.length;
286+
}
287+
if (inputType === 'deleteContentBackward') {
288+
// When the user does a backspace delete he expects the content before the cursor to be removed.
289+
// For this the start value needs to be adjusted (its as if the selection was before the text that we want to delete)
290+
start = Math.max(start - before, 0);
291+
}
292+
}
293+
294+
return {start, before, count};
295+
}
296+
297+
export {parseText, parseRangesToHTMLNodes, calculateInputMetrics};
227298

228-
export type {MarkdownRange, MarkdownType};
299+
export type {MarkdownRange, MarkdownType, Selection};

0 commit comments

Comments
 (0)