Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(web): handle text input metrics correctly on history undo events #434

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
34 changes: 5 additions & 29 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@ interface MarkdownNativeEvent extends Event {
inputType: string;
}

type Selection = {
hannojg marked this conversation as resolved.
Show resolved Hide resolved
start: number;
end: number;
};

type Dimensions = {
width: number;
height: number;
Expand Down Expand Up @@ -179,7 +174,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
const pasteRef = useRef<boolean>(false);
const divRef = useRef<HTMLDivElement | null>(null);
const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
const contentSelection = useRef<Selection | null>(null);
const contentSelection = useRef<CursorUtils.Selection | null>(null);
const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
const history = useRef<InputHistory>();
const dimensions = React.useRef<Dimensions | null>(null);
Expand Down Expand Up @@ -303,15 +298,15 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
[onSelectionChange, setEventProps],
);

const updateRefSelectionVariables = useCallback((newSelection: Selection) => {
const updateRefSelectionVariables = useCallback((newSelection: CursorUtils.Selection) => {
const {start, end} = newSelection;
const markdownHTMLInput = divRef.current as HTMLInputElement;
markdownHTMLInput.selectionStart = start;
markdownHTMLInput.selectionEnd = end;
}, []);

const updateSelection = useCallback(
(e: SyntheticEvent<HTMLDivElement> | null = null, predefinedSelection: Selection | null = null) => {
(e: SyntheticEvent<HTMLDivElement> | null = null, predefinedSelection: CursorUtils.Selection | null = null) => {
if (!divRef.current) {
return;
}
Expand Down Expand Up @@ -400,26 +395,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
}>;
setEventProps(event);

// The new text is between the prev start selection and the new end selection, can be empty
const addedText = normalizedText.slice(prevSelection.start, cursorPosition ?? 0);
// The length of the text that replaced the before text
const count = addedText.length;
// The start index of the replacement operation
let start = prevSelection.start;

const prevSelectionRange = prevSelection.end - prevSelection.start;
// The length the deleted text had before
let before = prevSelectionRange;
if (prevSelectionRange === 0 && (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward')) {
// 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
before = prevTextLength - normalizedText.length;
}

if (inputType === 'deleteContentBackward') {
// When the user does a backspace delete he expects the content before the cursor to be removed.
// For this the start value needs to be adjusted (its as if the selection was before the text that we want to delete)
start = Math.max(start - before, 0);
}
const {start, before, count} = ParseUtils.calculateInputMetrics(inputType, prevSelection, prevTextLength, normalizedText, cursorPosition);

event.nativeEvent.count = count;
event.nativeEvent.before = before;
Expand Down Expand Up @@ -660,7 +636,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
return;
}

const newSelection: Selection = {start: selection.start, end: selection.end ?? selection.start};
const newSelection: CursorUtils.Selection = {start: selection.start, end: selection.end ?? selection.start};
contentSelection.current = newSelection;
updateRefSelectionVariables(newSelection);
CursorUtils.setCursorPosition(divRef.current, newSelection.start, newSelection.end);
Expand Down
6 changes: 6 additions & 0 deletions src/web/cursorUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as BrowserUtils from './browserUtils';

type Selection = {
start: number;
end: number;
};

let prevTextLength: number | undefined;

function getPrevTextLength() {
Expand Down Expand Up @@ -162,4 +167,5 @@ function scrollCursorIntoView(target: HTMLInputElement) {
}
}

export type {Selection};
export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, setPrevText, removeSelection, scrollCursorIntoView, getPrevTextLength};
70 changes: 69 additions & 1 deletion src/web/parserUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
endIndex: number;
};

type TextChangeMetrics = {
/**
* The start index in the provided string where the repalcement started from.
*/
start: number;
/**
* The amount of characters that have been added.
*/
count: number;
/**
* The amount of characters replaced.
*/
before: number;
};

function addStyling(targetElement: HTMLElement, type: MarkdownType, markdownStyle: PartialMarkdownStyle) {
const node = targetElement;
switch (type) {
Expand Down Expand Up @@ -223,6 +238,59 @@
return {text: target.innerText, cursorPosition: cursorPosition || 0};
}

export {parseText, parseRangesToHTMLNodes};
/**
* Calculates start, count and before values. Whenever the text is being changed you can think of it as a replacement operation,
* where parts of the string get replaced with new content.
*
* This is to align the onChange event with the native counter part:
* - https://github.com/facebook/react-native/pull/45248
*/
function calculateInputMetrics(inputType: string, prevSelection: CursorUtils.Selection, prevTextLength: number, normalizedText: string, cursorPosition: number | null): TextChangeMetrics {
// The new text is between the prev start selection and the new end selection, can be empty
const addedText = normalizedText.slice(prevSelection.start, cursorPosition ?? 0);
// The length of the text that replaced the "before" text
const count = addedText.length;
// The start index of the replacement operation
let start = prevSelection.start;
// Before is by default the length of the previous selection
let before = prevSelection.end - prevSelection.start;

// For some events start and before need to be adjusted
if (inputType === 'historyUndo') {
// wip: not working yet
before = Math.abs(prevText.length - normalizedText.length);

Check failure on line 261 in src/web/parserUtils.ts

View workflow job for this annotation

GitHub Actions / check

Cannot find name 'prevText'. Did you mean 'parseText'?

count = 0;

Check failure on line 263 in src/web/parserUtils.ts

View workflow job for this annotation

GitHub Actions / check

Cannot assign to 'count' because it is a constant.
let startFound = false;
let charIndex = newCursorPosition - 1;

Check failure on line 265 in src/web/parserUtils.ts

View workflow job for this annotation

GitHub Actions / check

Cannot find name 'newCursorPosition'. Did you mean 'cursorPosition'?
while (!startFound) {
const newChar = normalizedText[charIndex];
const prevChar = prevText[charIndex];

Check failure on line 268 in src/web/parserUtils.ts

View workflow job for this annotation

GitHub Actions / check

Cannot find name 'prevText'. Did you mean 'parseText'?
charIndex--;

if (newChar !== prevChar) {
count++;

Check failure on line 272 in src/web/parserUtils.ts

View workflow job for this annotation

GitHub Actions / check

Cannot assign to 'count' because it is a constant.
} else {
startFound = count > 0 || charIndex === 0;
}
}
start = newCursorPosition - count;

Check failure on line 277 in src/web/parserUtils.ts

View workflow job for this annotation

GitHub Actions / check

Cannot find name 'newCursorPosition'. Did you mean 'cursorPosition'?
} else if (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward') {
if (before === 0) {
// Its possible the user pressed a delete key without a selection range (before = 0),
// so we need to adjust the before value to have the length of the deleted text
before = prevTextLength - normalizedText.length;
}
if (inputType === 'deleteContentBackward') {
// When the user does a backspace delete he expects the content before the cursor to be removed.
// For this the start value needs to be adjusted (its as if the selection was before the text that we want to delete)
start = Math.max(start - before, 0);
}
}

return {start, before, count};
}

export {parseText, parseRangesToHTMLNodes, calculateInputMetrics};

export type {MarkdownRange, MarkdownType};
Loading