@expensify/react-native-live-markdown 0.1.157 → 0.1.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/lib/commonjs/MarkdownTextInput.web.js +26 -6
  2. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  3. package/lib/commonjs/web/inputElements/inlineImage.js +1 -1
  4. package/lib/commonjs/web/inputElements/inlineImage.js.map +1 -1
  5. package/lib/commonjs/web/utils/cursorUtils.js +17 -3
  6. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -1
  7. package/lib/commonjs/web/utils/parserUtils.js +7 -14
  8. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  9. package/lib/commonjs/web/utils/treeUtils.js +13 -0
  10. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  11. package/lib/module/MarkdownTextInput.web.js +26 -6
  12. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  13. package/lib/module/web/inputElements/inlineImage.js +1 -1
  14. package/lib/module/web/inputElements/inlineImage.js.map +1 -1
  15. package/lib/module/web/utils/cursorUtils.js +17 -3
  16. package/lib/module/web/utils/cursorUtils.js.map +1 -1
  17. package/lib/module/web/utils/parserUtils.js +8 -15
  18. package/lib/module/web/utils/parserUtils.js.map +1 -1
  19. package/lib/module/web/utils/treeUtils.js +13 -1
  20. package/lib/module/web/utils/treeUtils.js.map +1 -1
  21. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  22. package/lib/typescript/src/web/utils/cursorUtils.d.ts +2 -2
  23. package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -1
  24. package/lib/typescript/src/web/utils/parserUtils.d.ts +1 -1
  25. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
  26. package/lib/typescript/src/web/utils/treeUtils.d.ts +2 -1
  27. package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/MarkdownTextInput.web.tsx +30 -5
  30. package/src/web/inputElements/inlineImage.ts +1 -1
  31. package/src/web/utils/cursorUtils.ts +18 -3
  32. package/src/web/utils/parserUtils.ts +9 -14
  33. package/src/web/utils/treeUtils.ts +14 -1
@@ -112,6 +112,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
112
112
  const history = useRef<InputHistory>();
113
113
  const dimensions = useRef<Dimensions | null>(null);
114
114
  const pasteContent = useRef<string | null>(null);
115
+ const hasJustBeenFocused = useRef<boolean>(false);
115
116
 
116
117
  if (!history.current) {
117
118
  history.current = new InputHistory(100, 150, value || '');
@@ -144,6 +145,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
144
145
  cursorPosition: number | null = null,
145
146
  shouldAddToHistory = true,
146
147
  shouldForceDOMUpdate = false,
148
+ shouldScrollIntoView = false,
147
149
  ): ParseTextResult => {
148
150
  if (!divRef.current) {
149
151
  return {text: text || '', cursorPosition: null};
@@ -152,7 +154,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
152
154
  if (text === null) {
153
155
  return {text: divRef.current.value, cursorPosition: null};
154
156
  }
155
- const parsedText = updateInputStructure(target, text, cursorPosition, multiline, customMarkdownStyles, false, shouldForceDOMUpdate);
157
+ const parsedText = updateInputStructure(target, text, cursorPosition, multiline, customMarkdownStyles, false, shouldForceDOMUpdate, shouldScrollIntoView);
156
158
  divRef.current.value = parsedText.text;
157
159
 
158
160
  if (history.current && shouldAddToHistory) {
@@ -262,6 +264,19 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
262
264
  [handleSelectionChange, updateRefSelectionVariables],
263
265
  );
264
266
 
267
+ const handleOnSelect = useCallback(
268
+ (e) => {
269
+ updateSelection(e);
270
+
271
+ // If the input has just been focused, we need to scroll the cursor into view
272
+ if (divRef.current && contentSelection.current && hasJustBeenFocused.current) {
273
+ setCursorPosition(divRef.current, contentSelection.current?.start, contentSelection.current?.end, true);
274
+ hasJustBeenFocused.current = false;
275
+ }
276
+ },
277
+ [updateSelection],
278
+ );
279
+
265
280
  const handleContentSizeChange = useCallback(() => {
266
281
  if (!divRef.current || !multiline || !onContentSizeChange) {
267
282
  return;
@@ -311,6 +326,9 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
311
326
  end: newCursorPosition,
312
327
  });
313
328
  divRef.current.value = parsedText;
329
+ if (onChangeText) {
330
+ onChangeText(parsedText);
331
+ }
314
332
  return;
315
333
  }
316
334
 
@@ -323,7 +341,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
323
341
  newInputUpdate = redo(divRef.current);
324
342
  break;
325
343
  default:
326
- newInputUpdate = parseText(divRef.current, parsedText, processedMarkdownStyle, newCursorPosition, true, !inputType);
344
+ newInputUpdate = parseText(divRef.current, parsedText, processedMarkdownStyle, newCursorPosition, true, !inputType, inputType === 'pasteText');
327
345
  }
328
346
  const {text, cursorPosition} = newInputUpdate;
329
347
  updateTextColor(divRef.current, text);
@@ -457,6 +475,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
457
475
 
458
476
  const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
459
477
  (event) => {
478
+ hasJustBeenFocused.current = true;
460
479
  const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
461
480
  const hostNode = e.target as unknown as HTMLDivElement;
462
481
  currentlyFocusedField.current = hostNode;
@@ -465,11 +484,17 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
465
484
  if (contentSelection.current) {
466
485
  setCursorPosition(divRef.current, contentSelection.current.start, contentSelection.current.end);
467
486
  } else {
468
- const valueLength = value ? value.length : divRef.current.value.length;
487
+ const valueLength = value ? value.length : (divRef.current.value || '').length;
469
488
  setCursorPosition(divRef.current, valueLength, null);
470
489
  }
471
490
  }
472
491
 
492
+ if (divRef.current) {
493
+ divRef.current.scrollIntoView({
494
+ block: 'nearest',
495
+ });
496
+ }
497
+
473
498
  if (onFocus) {
474
499
  setEventProps(e);
475
500
  onFocus(e);
@@ -613,7 +638,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
613
638
  }
614
639
  const normalizedValue = normalizeValue(value);
615
640
  divRef.current.value = normalizedValue;
616
- parseText(divRef.current, normalizedValue, processedMarkdownStyle);
641
+ parseText(divRef.current, normalizedValue, processedMarkdownStyle, null, true, false, true);
617
642
  updateTextColor(divRef.current, value);
618
643
  },
619
644
  [multiline, processedMarkdownStyle, value],
@@ -697,7 +722,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
697
722
  spellCheck={spellCheck}
698
723
  dir={dir}
699
724
  inputMode={inputMode}
700
- onSelect={updateSelection}
725
+ onSelect={handleOnSelect}
701
726
  onTouchStart={handleTouchStart}
702
727
  />
703
728
  );
@@ -95,7 +95,7 @@ function createImageElement(currentInput: MarkdownTextInputElement, targetNode:
95
95
  if (timeoutMap.has(targetNode.orderIndex)) {
96
96
  const mapItem = timeoutMap.get(targetNode.orderIndex);
97
97
  // Check if the image URL has been changed, if not, early return so the image can be loaded asynchronously
98
- const currentElement = document.querySelector(`[data-type="block"][data-id="${targetNode.orderIndex}"]`) as HTMLMarkdownElement;
98
+ const currentElement = currentInput.querySelector(`[data-type="block"][data-id="${targetNode.orderIndex}"]`) as HTMLMarkdownElement;
99
99
  if (mapItem?.url === url && currentElement && getImagePreviewElement(currentElement)) {
100
100
  return;
101
101
  }
@@ -2,7 +2,7 @@ import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
2
  import {findHTMLElementInTree, getTreeNodeByIndex} from './treeUtils';
3
3
  import type {TreeNode} from './treeUtils';
4
4
 
5
- function setCursorPosition(target: MarkdownTextInputElement, startIndex: number, endIndex: number | null = null) {
5
+ function setCursorPosition(target: MarkdownTextInputElement, startIndex: number, endIndex: number | null = null, shouldScrollIntoView = false) {
6
6
  // We don't want to move the cursor if the target is not focused
7
7
  if (!target.tree || target !== document.activeElement) {
8
8
  return;
@@ -45,16 +45,31 @@ function setCursorPosition(target: MarkdownTextInputElement, startIndex: number,
45
45
  selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
46
46
  }
47
47
 
48
- scrollIntoView(startTreeNode);
48
+ if (shouldScrollIntoView) {
49
+ scrollIntoView(target, endTreeNode);
50
+ }
49
51
  }
50
52
 
51
- function scrollIntoView(node: TreeNode) {
53
+ function scrollIntoView(target: MarkdownTextInputElement, node: TreeNode) {
54
+ const targetElement = target;
52
55
  if (node.type === 'br' && node.parentNode?.parentNode?.type === 'line') {
53
56
  // If the node is a line break, scroll to the parent paragraph, because Safari doesn't support scrollIntoView on br elements
54
57
  node.parentNode.parentNode.element.scrollIntoView({
55
58
  block: 'nearest',
56
59
  });
57
60
  } else {
61
+ const selection = window.getSelection();
62
+ if (selection && selection.rangeCount > 0) {
63
+ const range = selection.getRangeAt(0);
64
+ const caretRect = range.getBoundingClientRect();
65
+ const targetRect = target.getBoundingClientRect();
66
+
67
+ // In case the caret is below the visible input area, scroll to the end of the node
68
+ if (caretRect.top + caretRect.height > targetRect.top + targetRect.height) {
69
+ targetElement.scrollTop = caretRect.top + caretRect.height - targetRect.top - targetRect.height + target.scrollTop;
70
+ }
71
+ }
72
+
58
73
  node.element.scrollIntoView({
59
74
  block: 'nearest',
60
75
  });
@@ -1,5 +1,5 @@
1
1
  import type {HTMLMarkdownElement, MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
- import {addNodeToTree, updateTreeElementRefs} from './treeUtils';
2
+ import {addNodeToTree, createRootTreeNode, updateTreeElementRefs} from './treeUtils';
3
3
  import type {NodeType, TreeNode} from './treeUtils';
4
4
  import type {PartialMarkdownStyle} from '../../styleUtils';
5
5
  import {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition} from './cursorUtils';
@@ -154,16 +154,8 @@ function parseRangesToHTMLNodes(
154
154
  ) {
155
155
  const rootElement: HTMLMarkdownElement = document.createElement('span') as HTMLMarkdownElement;
156
156
  const textLength = text.length;
157
- const rootNode: TreeNode = {
158
- element: rootElement,
159
- start: 0,
160
- length: textLength,
161
- parentNode: null,
162
- childNodes: [],
163
- type: 'root',
164
- orderIndex: '',
165
- isGeneratingNewline: false,
166
- };
157
+ const rootNode: TreeNode = createRootTreeNode(rootElement, textLength);
158
+
167
159
  let currentParentNode: TreeNode = rootNode;
168
160
  let lines = splitTextIntoLines(text);
169
161
 
@@ -266,7 +258,7 @@ function parseRangesToHTMLNodes(
266
258
  return {dom: rootElement, tree: rootNode};
267
259
  }
268
260
 
269
- function moveCursor(isFocused: boolean, alwaysMoveCursorToTheEnd: boolean, cursorPosition: number | null, target: MarkdownTextInputElement) {
261
+ function moveCursor(isFocused: boolean, alwaysMoveCursorToTheEnd: boolean, cursorPosition: number | null, target: MarkdownTextInputElement, shouldScrollIntoView = false) {
270
262
  if (!isFocused) {
271
263
  return;
272
264
  }
@@ -274,7 +266,7 @@ function moveCursor(isFocused: boolean, alwaysMoveCursorToTheEnd: boolean, curso
274
266
  if (alwaysMoveCursorToTheEnd || cursorPosition === null) {
275
267
  moveCursorToEnd(target);
276
268
  } else if (cursorPosition !== null) {
277
- setCursorPosition(target, cursorPosition);
269
+ setCursorPosition(target, cursorPosition, null, shouldScrollIntoView);
278
270
  }
279
271
  }
280
272
 
@@ -286,6 +278,7 @@ function updateInputStructure(
286
278
  markdownStyle: PartialMarkdownStyle = {},
287
279
  alwaysMoveCursorToTheEnd = false,
288
280
  shouldForceDOMUpdate = false,
281
+ shouldScrollIntoView = false,
289
282
  ) {
290
283
  const targetElement = target;
291
284
 
@@ -317,7 +310,9 @@ function updateInputStructure(
317
310
  updateTreeElementRefs(tree, targetElement);
318
311
  targetElement.tree = tree;
319
312
 
320
- moveCursor(isFocused, alwaysMoveCursorToTheEnd, cursorPosition, targetElement);
313
+ moveCursor(isFocused, alwaysMoveCursorToTheEnd, cursorPosition, targetElement, shouldScrollIntoView);
314
+ } else {
315
+ targetElement.tree = createRootTreeNode(targetElement);
321
316
  }
322
317
 
323
318
  return {text, cursorPosition: cursorPosition || 0};
@@ -12,6 +12,19 @@ type TreeNode = Omit<MarkdownRange, 'type'> & {
12
12
  isGeneratingNewline: boolean;
13
13
  };
14
14
 
15
+ function createRootTreeNode(target: HTMLMarkdownElement, length = 0): TreeNode {
16
+ return {
17
+ element: target,
18
+ start: 0,
19
+ length,
20
+ parentNode: null,
21
+ childNodes: [],
22
+ type: 'root',
23
+ orderIndex: '',
24
+ isGeneratingNewline: false,
25
+ };
26
+ }
27
+
15
28
  function addNodeToTree(element: HTMLMarkdownElement, parentTreeNode: TreeNode, type: NodeType, length: number | null = null) {
16
29
  const contentLength = length || (element.nodeName === 'BR' || type === 'br' ? 1 : element.value?.length) || 0;
17
30
  const isGeneratingNewline = type === 'line' && !(element.childNodes.length === 1 && (element.childNodes[0] as HTMLElement)?.getAttribute?.('data-type') === 'br');
@@ -115,6 +128,6 @@ function getTreeNodeByIndex(treeRoot: TreeNode, index: number): TreeNode | null
115
128
  return null;
116
129
  }
117
130
 
118
- export {addNodeToTree, findHTMLElementInTree, getTreeNodeByIndex, updateTreeElementRefs};
131
+ export {addNodeToTree, findHTMLElementInTree, getTreeNodeByIndex, updateTreeElementRefs, createRootTreeNode};
119
132
 
120
133
  export type {TreeNode, NodeType};