@expensify/react-native-live-markdown 0.1.16 → 0.1.18

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.
@@ -150,15 +150,18 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
150
150
  placeholderTextColor = `rgba(0,0,0,0.2)`,
151
151
  selectTextOnFocus,
152
152
  spellCheck,
153
+ selection,
153
154
  style = {},
154
155
  value,
155
156
  autoFocus = false,
156
157
  },
157
158
  ref,
158
159
  ) => {
160
+ const compositionRef = useRef<boolean>(false);
159
161
  const divRef = useRef<HTMLDivElement | null>(null);
160
162
  const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
161
- const contentSelection = useRef<Selection | null>(null);
163
+ const valueLength = value ? value.length : 0;
164
+ const contentSelection = useRef<Selection>({start: valueLength, end: valueLength});
162
165
  const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
163
166
  const history = useRef<InputHistory>();
164
167
  if (!history.current) {
@@ -170,15 +173,18 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
170
173
  // Empty placeholder would collapse the div, so we need to use zero-width space to prevent it
171
174
  const heightSafePlaceholder = useMemo(() => getPlaceholderValue(placeholder), [placeholder]);
172
175
 
173
- const updateSelection = useCallback(() => {
174
- if (!divRef.current) {
175
- return;
176
+ const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
177
+ if (divRef.current) {
178
+ const text = normalizeValue(divRef.current.innerText || '');
179
+ if (e.target) {
180
+ // TODO: change the logic here so every event have value property
181
+ (e.target as unknown as HTMLInputElement).value = text;
182
+ }
183
+ if (e.nativeEvent && e.nativeEvent.text) {
184
+ e.nativeEvent.text = text;
185
+ }
176
186
  }
177
- const selection = CursorUtils.getCurrentCursorPosition(divRef.current);
178
- const markdownHTMLInput = divRef.current as HTMLInputElement;
179
- markdownHTMLInput.selectionStart = selection.start;
180
- markdownHTMLInput.selectionEnd = selection.end;
181
- contentSelection.current = selection;
187
+ return e;
182
188
  }, []);
183
189
 
184
190
  const parseText = useCallback(
@@ -191,8 +197,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
191
197
  history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition);
192
198
  }
193
199
 
194
- updateSelection();
195
-
196
200
  return parsedText;
197
201
  },
198
202
  [multiline],
@@ -245,18 +249,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
245
249
  return value;
246
250
  }, [value]);
247
251
 
248
- const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
249
- if (divRef.current) {
250
- const text = normalizeValue(divRef.current.innerText || '');
251
- if (typeof e.target !== 'number') {
252
- // TODO: change the logic here so every event have value property
253
- (e.target as unknown as HTMLInputElement).value = text;
254
- }
255
- e.nativeEvent.text = text;
256
- }
257
- return e;
258
- }, []);
259
-
260
252
  // Placeholder text color logic
261
253
  const updateTextColor = useCallback((node: HTMLDivElement, text: string) => {
262
254
  // eslint-disable-next-line no-param-reassign -- we need to change the style of the node, so we need to modify it
@@ -269,6 +261,12 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
269
261
  return;
270
262
  }
271
263
 
264
+ if (compositionRef.current) {
265
+ updateTextColor(divRef.current, e.target.innerText);
266
+ compositionRef.current = false;
267
+ return;
268
+ }
269
+
272
270
  let text = '';
273
271
  const nativeEvent = e.nativeEvent as MarkdownNativeEvent;
274
272
  switch (nativeEvent.inputType) {
@@ -297,6 +295,41 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
297
295
  [multiline, onChange, onChangeText, setEventProps, processedMarkdownStyle],
298
296
  );
299
297
 
298
+ const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
299
+ (event) => {
300
+ const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>;
301
+ setEventProps(e);
302
+ if (onSelectionChange && contentSelection.current) {
303
+ e.nativeEvent.selection = contentSelection.current;
304
+ onSelectionChange(e);
305
+ }
306
+ },
307
+ [onSelectionChange, setEventProps],
308
+ );
309
+
310
+ const updateRefSelectionVariables = useCallback((newSelection: Selection) => {
311
+ const {start, end} = newSelection;
312
+ const markdownHTMLInput = divRef.current as HTMLInputElement;
313
+ markdownHTMLInput.selectionStart = start;
314
+ markdownHTMLInput.selectionEnd = end;
315
+ }, []);
316
+
317
+ const updateSelection = useCallback((e: SyntheticEvent<HTMLDivElement> | null = null) => {
318
+ if (!divRef.current) {
319
+ return;
320
+ }
321
+ const newSelection = CursorUtils.getCurrentCursorPosition(divRef.current);
322
+
323
+ if (newSelection && (contentSelection.current.start !== newSelection.start || contentSelection.current.end !== newSelection.end)) {
324
+ updateRefSelectionVariables(newSelection);
325
+ contentSelection.current = newSelection;
326
+
327
+ if (e) {
328
+ handleSelectionChange(e);
329
+ }
330
+ }
331
+ }, []);
332
+
300
333
  const handleKeyPress = useCallback(
301
334
  (e: KeyboardEvent<HTMLDivElement>) => {
302
335
  if (!divRef.current) {
@@ -331,6 +364,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
331
364
  onKeyPress(event);
332
365
  }
333
366
 
367
+ updateSelection(event as unknown as SyntheticEvent<HTMLDivElement, Event>);
368
+
334
369
  if (
335
370
  e.key === 'Enter' &&
336
371
  !e.shiftKey &&
@@ -359,19 +394,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
359
394
  [onKeyPress],
360
395
  );
361
396
 
362
- const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
363
- (event) => {
364
- const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>;
365
- setEventProps(e);
366
- updateSelection();
367
- if (onSelectionChange && contentSelection.current) {
368
- e.nativeEvent.selection = contentSelection.current;
369
- onSelectionChange(e);
370
- }
371
- },
372
- [onSelectionChange, setEventProps],
373
- );
374
-
375
397
  const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
376
398
  (event) => {
377
399
  const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
@@ -379,7 +401,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
379
401
  currentlyFocusedField.current = hostNode;
380
402
  setEventProps(e);
381
403
  if (divRef.current && contentSelection.current) {
382
- CursorUtils.setCursorPosition(divRef.current, contentSelection.current.start || divRef.current.innerText.length, !multiline);
404
+ CursorUtils.setCursorPosition(divRef.current, contentSelection.current.end || contentSelection.current.start);
383
405
  }
384
406
 
385
407
  if (onFocus) {
@@ -411,6 +433,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
411
433
  const handleBlur: FocusEventHandler<HTMLDivElement> = useCallback(
412
434
  (event) => {
413
435
  const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
436
+ CursorUtils.removeSelection();
414
437
  currentlyFocusedField.current = null;
415
438
  if (onBlur) {
416
439
  setEventProps(e);
@@ -422,7 +445,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
422
445
 
423
446
  const handleClick = useCallback(
424
447
  (e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
425
- updateSelection();
448
+ updateSelection(e);
426
449
  if (!onClick || !divRef.current) {
427
450
  return;
428
451
  }
@@ -432,6 +455,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
432
455
  [onClick],
433
456
  );
434
457
 
458
+ const startComposition = useCallback(() => {
459
+ compositionRef.current = true;
460
+ }, []);
461
+
435
462
  const setRef = (currentRef: HTMLDivElement | null) => {
436
463
  const r = currentRef;
437
464
  if (r) {
@@ -507,13 +534,24 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
507
534
  }, []);
508
535
 
509
536
  useEffect(() => {
510
- // focus the input on mount if autoFocus is set
511
- if (!(divRef.current && autoFocus)) {
537
+ if (!divRef.current) {
512
538
  return;
513
539
  }
514
- divRef.current.focus();
540
+ // focus the input on mount if autoFocus is set
541
+ if (autoFocus) {
542
+ divRef.current.focus();
543
+ }
544
+ updateRefSelectionVariables(contentSelection.current);
515
545
  }, []);
516
546
 
547
+ useEffect(() => {
548
+ if (!divRef.current || !selection || (selection.start === contentSelection.current.start && selection.end === contentSelection.current.end)) {
549
+ return;
550
+ }
551
+ CursorUtils.setCursorPosition(divRef.current, selection.start, selection.end);
552
+ updateSelection();
553
+ }, [selection]);
554
+
517
555
  return (
518
556
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
519
557
  <div
@@ -529,9 +567,9 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
529
567
  autoCapitalize={autoCapitalize}
530
568
  className={className}
531
569
  onKeyDown={handleKeyPress}
570
+ onCompositionStart={startComposition}
532
571
  onKeyUp={updateSelection}
533
572
  onInput={handleOnChangeText}
534
- onSelect={handleSelectionChange}
535
573
  onClick={handleClick}
536
574
  onFocus={handleFocus}
537
575
  onBlur={handleBlur}
@@ -552,8 +590,8 @@ const styles = StyleSheet.create({
552
590
  // @ts-expect-error it works on web
553
591
  boxSizing: 'border-box',
554
592
  whiteSpace: 'pre-wrap',
555
- overflowY: 'scroll',
556
- overflowX: 'scroll',
593
+ overflowY: 'auto',
594
+ overflowX: 'auto',
557
595
  overflowWrap: 'break-word',
558
596
  },
559
597
  disabledInputStyles: {
@@ -1,46 +1,53 @@
1
- function createRange(node: HTMLElement, targetPosition: number, ignoreNewLines = false) {
1
+ function findTextNodes(textNodes: Text[], node: ChildNode) {
2
+ if (node.nodeType === Node.TEXT_NODE) {
3
+ textNodes.push(node as Text);
4
+ } else {
5
+ for (let i = 0, length = node.childNodes.length; i < length; ++i) {
6
+ const childNode = node.childNodes[i];
7
+ if (childNode) {
8
+ findTextNodes(textNodes, childNode);
9
+ }
10
+ }
11
+ }
12
+ }
13
+
14
+ function setCursorPosition(target: HTMLElement, start: number, end: number | null = null) {
2
15
  const range = document.createRange();
3
- range.selectNode(node);
16
+ range.selectNodeContents(target);
4
17
 
5
- let pos = 0;
6
- const stack: Node[] = [node];
7
- while (stack.length > 0) {
8
- const current = stack.pop();
9
- if (!current) {
10
- break;
11
- }
12
- if (current.nodeType === Node.TEXT_NODE || current.nodeName === 'BR') {
13
- const textContentLength = current.textContent ? current.textContent.length : 0;
14
- const len = current.nodeName === 'BR' ? 1 : textContentLength;
15
- if (pos + len >= targetPosition) {
16
- if (current.nodeName === 'BR') {
17
- range.setStartAfter(current);
18
- (current as HTMLElement).scrollIntoView();
19
- } else {
20
- range.setStart(current, targetPosition - pos);
18
+ const textNodes: Text[] = [];
19
+ findTextNodes(textNodes, target);
20
+
21
+ let charCount = 0;
22
+ let startNode: Text | null = null;
23
+ let endNode: Text | null = null;
24
+ const n = textNodes.length;
25
+ for (let i = 0; i < n; ++i) {
26
+ const textNode = textNodes[i];
27
+ if (textNode) {
28
+ const nextCharCount = charCount + textNode.length;
29
+
30
+ if (!startNode && start >= charCount && (start <= nextCharCount || (start === nextCharCount && i < n - 1))) {
31
+ startNode = textNode;
32
+ range.setStart(textNode, start - charCount);
33
+ if (!end) {
34
+ break;
21
35
  }
22
- return range;
23
36
  }
24
- pos += len;
25
- } else if (current.childNodes && current.childNodes.length > 0) {
26
- for (let i = current.childNodes.length - 1; i >= 0; i--) {
27
- const currentNode = current.childNodes[i];
28
- if (currentNode && (!ignoreNewLines || (ignoreNewLines && currentNode.nodeName !== 'BR'))) {
29
- stack.push(currentNode);
30
- }
37
+ if (end && !endNode && end >= charCount && (end <= nextCharCount || (end === nextCharCount && i < n - 1))) {
38
+ endNode = textNode;
39
+ range.setEnd(textNode, end - charCount);
31
40
  }
41
+ charCount = nextCharCount;
32
42
  }
33
43
  }
34
44
 
35
- range.setStart(node, node.childNodes.length);
36
- return range;
37
- }
45
+ if (!end) {
46
+ range.collapse(true);
47
+ }
38
48
 
39
- function setCursorPosition(target: HTMLElement, targetPosition: number, ignoreNewLines = false) {
40
- const range = createRange(target, targetPosition, ignoreNewLines);
41
49
  const selection = window.getSelection();
42
50
  if (selection) {
43
- range.collapse(true);
44
51
  selection.removeAllRanges();
45
52
  selection.addRange(range);
46
53
  }
@@ -57,31 +64,25 @@ function moveCursorToEnd(target: HTMLElement) {
57
64
  }
58
65
  }
59
66
 
60
- function getIndexedPosition(target: HTMLElement, range: Range, isStart: boolean) {
61
- const marker = document.createTextNode('\0');
62
- const rangeClone = range.cloneRange();
63
-
64
- rangeClone.collapse(isStart);
65
-
66
- rangeClone.insertNode(marker);
67
- const position = target.innerText.indexOf('\0');
68
- if (marker.parentNode) {
69
- marker.parentNode.removeChild(marker);
70
- }
71
-
72
- return position;
73
- }
74
-
75
67
  function getCurrentCursorPosition(target: HTMLElement) {
76
- const selection = document.getSelection();
77
- if (!selection || selection.rangeCount === 0) {
78
- return {start: target.innerText.length, end: target.innerText.length};
68
+ const selection = window.getSelection();
69
+ if (!selection || (selection && selection.rangeCount === 0)) {
70
+ return null;
79
71
  }
80
-
81
72
  const range = selection.getRangeAt(0);
82
- const start = getIndexedPosition(target, range, true);
83
- const end = getIndexedPosition(target, range, false);
73
+ const preSelectionRange = range.cloneRange();
74
+ preSelectionRange.selectNodeContents(target);
75
+ preSelectionRange.setEnd(range.startContainer, range.startOffset);
76
+ const start = preSelectionRange.toString().length;
77
+ const end = start + range.toString().length;
84
78
  return {start, end};
85
79
  }
86
80
 
87
- export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition};
81
+ function removeSelection() {
82
+ const selection = window.getSelection();
83
+ if (selection) {
84
+ selection.removeAllRanges();
85
+ }
86
+ }
87
+
88
+ export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, removeSelection};
@@ -160,20 +160,14 @@ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownS
160
160
  return root;
161
161
  }
162
162
 
163
- function parseText(
164
- target: HTMLElement,
165
- text: string,
166
- curosrPositionIndex: number | null,
167
- markdownStyle: PartialMarkdownStyle = {},
168
- disableNewLinesInCursorPositioning = false,
169
- alwaysMoveCursorToTheEnd = false,
170
- ) {
163
+ function parseText(target: HTMLElement, text: string, curosrPositionIndex: number | null, markdownStyle: PartialMarkdownStyle = {}, alwaysMoveCursorToTheEnd = false) {
171
164
  const targetElement = target;
172
165
 
173
166
  let cursorPosition: number | null = curosrPositionIndex;
174
167
  const isFocused = document.activeElement === target;
175
168
  if (isFocused && curosrPositionIndex === null) {
176
- cursorPosition = CursorUtils.getCurrentCursorPosition(target).start;
169
+ const selection = CursorUtils.getCurrentCursorPosition(target);
170
+ cursorPosition = selection ? selection.end : null;
177
171
  }
178
172
  const ranges = global.parseExpensiMarkToRanges(text);
179
173
 
@@ -188,10 +182,12 @@ function parseText(
188
182
  target.appendChild(dom);
189
183
  }
190
184
 
191
- if (alwaysMoveCursorToTheEnd) {
192
- CursorUtils.moveCursorToEnd(target);
193
- } else if (isFocused && cursorPosition !== null) {
194
- CursorUtils.setCursorPosition(target, cursorPosition, disableNewLinesInCursorPositioning);
185
+ if (isFocused) {
186
+ if (alwaysMoveCursorToTheEnd || cursorPosition === null) {
187
+ CursorUtils.moveCursorToEnd(target);
188
+ } else if (cursorPosition !== null) {
189
+ CursorUtils.setCursorPosition(target, cursorPosition);
190
+ }
195
191
  }
196
192
 
197
193
  return {text: target.innerText, cursorPosition: cursorPosition || 0};