@expensify/react-native-live-markdown 0.1.70 → 0.1.72

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.
@@ -80,6 +80,10 @@ let focusTimeout: NodeJS.Timeout | null = null;
80
80
  function normalizeValue(value: string) {
81
81
  return value.replace(/\n$/, '');
82
82
  }
83
+ // Adds one '\n' at the end of the string if it's missing
84
+ function denormalizeValue(value: string) {
85
+ return value.endsWith('\n') ? `${value}\n` : value;
86
+ }
83
87
 
84
88
  // If an Input Method Editor is processing key input, the 'keyCode' is 229.
85
89
  // https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
@@ -174,7 +178,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
174
178
  const dimensions = React.useRef<Dimensions | null>(null);
175
179
 
176
180
  if (!history.current) {
177
- history.current = new InputHistory(100);
181
+ history.current = new InputHistory(100, 150, value || '');
178
182
  }
179
183
 
180
184
  const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]);
@@ -203,7 +207,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
203
207
  }
204
208
  const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline);
205
209
  if (history.current && shouldAddToHistory) {
206
- history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition);
210
+ // We need to normalize the value before saving it to the history to prevent situations when additional new lines break the cursor position calculation logic
211
+ history.current.throttledAdd(normalizeValue(parsedText.text), parsedText.cursorPosition);
207
212
  }
208
213
 
209
214
  return parsedText;
@@ -236,7 +241,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
236
241
  (target: HTMLDivElement) => {
237
242
  if (!history.current) return '';
238
243
  const item = history.current.undo();
239
- return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
244
+ const undoValue = item ? denormalizeValue(item.text) : null;
245
+ return parseText(target, undoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
240
246
  },
241
247
  [parseText, processedMarkdownStyle],
242
248
  );
@@ -245,7 +251,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
245
251
  (target: HTMLDivElement) => {
246
252
  if (!history.current) return '';
247
253
  const item = history.current.redo();
248
- return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
254
+ const redoValue = item ? denormalizeValue(item.text) : null;
255
+ return parseText(target, redoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
249
256
  },
250
257
  [parseText, processedMarkdownStyle],
251
258
  );
@@ -328,9 +335,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
328
335
  if (!divRef.current || !(e.target instanceof HTMLElement)) {
329
336
  return;
330
337
  }
338
+ const changedText = e.target.innerText;
331
339
 
332
340
  if (compositionRef.current) {
333
- updateTextColor(divRef.current, e.target.innerText);
341
+ updateTextColor(divRef.current, changedText);
334
342
  compositionRef.current = false;
335
343
  return;
336
344
  }
@@ -344,14 +352,22 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
344
352
  case 'historyRedo':
345
353
  text = redo(divRef.current);
346
354
  break;
355
+ case 'insertFromPaste':
356
+ // if there is no newline at the end of the copied text, contentEditable adds invisible <br> tag at the end of the text, so we need to normalize it
357
+ if (changedText.length > 2 && changedText[changedText.length - 2] !== '\n' && changedText[changedText.length - 1] === '\n') {
358
+ text = parseText(divRef.current, normalizeValue(changedText), processedMarkdownStyle).text;
359
+ break;
360
+ }
361
+ text = parseText(divRef.current, changedText, processedMarkdownStyle).text;
362
+ break;
347
363
  default:
348
- text = parseText(divRef.current, e.target.innerText, processedMarkdownStyle).text;
364
+ text = parseText(divRef.current, changedText, processedMarkdownStyle).text;
349
365
  }
350
366
  if (pasteRef?.current) {
351
367
  pasteRef.current = false;
352
368
  updateSelection(e);
353
369
  }
354
- updateTextColor(divRef.current, e.target.innerText);
370
+ updateTextColor(divRef.current, text);
355
371
 
356
372
  if (onChange) {
357
373
  const event = e as unknown as NativeSyntheticEvent<any>;
@@ -587,17 +603,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
587
603
  CursorUtils.setCursorPosition(divRef.current, newSelection.start, newSelection.end);
588
604
  }, [selection, updateRefSelectionVariables]);
589
605
 
590
- useEffect(() => {
591
- if (history.current?.history.length !== 0) {
592
- return;
593
- }
594
- const currentValue = value ?? '';
595
- history.current.add(currentValue, currentValue.length);
596
-
597
- handleContentSizeChange();
598
- // eslint-disable-next-line react-hooks/exhaustive-deps
599
- }, []);
600
-
601
606
  return (
602
607
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
603
608
  <div
@@ -6,7 +6,7 @@ type HistoryItem = {
6
6
  export default class InputHistory {
7
7
  depth: number;
8
8
 
9
- history: HistoryItem[];
9
+ items: HistoryItem[];
10
10
 
11
11
  historyIndex: number;
12
12
 
@@ -16,19 +16,19 @@ export default class InputHistory {
16
16
 
17
17
  debounceTime: number;
18
18
 
19
- constructor(depth: number, debounceTime = 150) {
19
+ constructor(depth: number, debounceTime = 150, startingText = '') {
20
20
  this.depth = depth;
21
- this.history = [];
21
+ this.items = [{text: startingText, cursorPosition: startingText.length}];
22
22
  this.historyIndex = 0;
23
23
  this.debounceTime = debounceTime;
24
24
  }
25
25
 
26
26
  getCurrentItem(): HistoryItem | null {
27
- return this.history[this.historyIndex] || null;
27
+ return this.items[this.historyIndex] || null;
28
28
  }
29
29
 
30
30
  setHistory(newHistory: HistoryItem[]): void {
31
- this.history = newHistory.slice(newHistory.length - this.depth);
31
+ this.items = newHistory.slice(newHistory.length - this.depth);
32
32
  this.historyIndex = newHistory.length - 1;
33
33
  }
34
34
 
@@ -37,80 +37,99 @@ export default class InputHistory {
37
37
  }
38
38
 
39
39
  clear(): void {
40
- this.history = [];
40
+ this.items = [];
41
41
  this.historyIndex = 0;
42
42
  }
43
43
 
44
- debouncedAdd(text: string, cursorPosition: number): void {
45
- this.currentText = text;
46
-
44
+ throttledAdd(text: string, cursorPosition: number): void {
47
45
  if (this.timeout) {
48
46
  clearTimeout(this.timeout);
49
47
  }
50
48
 
49
+ if (this.currentText === null) {
50
+ this.timeout = null;
51
+ this.add(text, cursorPosition);
52
+ } else {
53
+ this.items[this.historyIndex] = {text, cursorPosition};
54
+ }
55
+ this.currentText = text;
56
+
51
57
  this.timeout = setTimeout(() => {
52
- if (this.currentText == null) {
53
- return;
54
- }
55
- this.add(this.currentText, cursorPosition);
56
58
  this.currentText = null;
57
59
  }, this.debounceTime);
58
60
  }
59
61
 
62
+ stopTimeout(): void {
63
+ if (this.timeout) {
64
+ clearTimeout(this.timeout);
65
+ this.timeout = null;
66
+ }
67
+ this.currentText = null;
68
+ }
69
+
60
70
  add(text: string, cursorPosition: number): void {
61
- if (this.history.length > 0) {
62
- const lastItem = this.history[this.history.length - 1];
63
- if (lastItem && text === lastItem.text) {
64
- this.historyIndex = this.history.length - 1;
71
+ if (this.items.length > 0) {
72
+ const currentItem = this.items[this.historyIndex];
73
+ if (currentItem && text === currentItem.text) {
65
74
  return;
66
75
  }
67
76
  }
68
77
 
69
- if (this.historyIndex < this.history.length - 1) {
70
- this.history.splice(this.historyIndex + 1);
78
+ if (this.historyIndex < this.items.length - 1) {
79
+ this.items.splice(this.historyIndex + 1);
80
+ this.historyIndex = this.items.length - 1;
71
81
  }
72
82
 
73
- this.history.push({text, cursorPosition});
74
- if (this.history.length > this.depth) {
75
- this.history.shift();
83
+ this.items.push({text, cursorPosition});
84
+ if (this.items.length > this.depth) {
85
+ this.items.shift();
86
+ } else {
87
+ this.historyIndex += 1;
76
88
  }
77
-
78
- this.historyIndex = this.history.length - 1;
79
89
  }
80
90
 
81
91
  undo(): HistoryItem | null {
82
- if (this.currentText !== null && this.timeout) {
83
- clearTimeout(this.timeout);
84
- this.timeout = null;
85
- return this.history[this.historyIndex] || null;
86
- }
92
+ this.stopTimeout();
87
93
 
88
- if (this.history.length === 0 || this.historyIndex - 1 < 0) {
94
+ if (this.items.length === 0 || this.historyIndex - 1 < 0) {
89
95
  return null;
90
96
  }
91
97
 
98
+ const currentHistoryItem = this.items[this.historyIndex];
99
+ const previousHistoryItem = this.items[this.historyIndex - 1];
100
+
101
+ const undoItem = previousHistoryItem
102
+ ? {
103
+ text: previousHistoryItem.text,
104
+ cursorPosition: Math.min(
105
+ (currentHistoryItem?.cursorPosition ?? 0) - ((currentHistoryItem?.text ?? '').length - (previousHistoryItem?.text ?? '').length),
106
+ (previousHistoryItem?.text ?? '').length,
107
+ ),
108
+ }
109
+ : null;
110
+
92
111
  if (this.historyIndex > 0) {
93
112
  this.historyIndex -= 1;
94
113
  }
95
- return this.history[this.historyIndex] || null;
114
+
115
+ return undoItem;
96
116
  }
97
117
 
98
118
  redo(): HistoryItem | null {
99
119
  if (this.currentText !== null && this.timeout) {
100
- clearTimeout(this.timeout);
101
- return this.history[this.history.length - 1] || null;
120
+ this.stopTimeout();
102
121
  }
103
122
 
104
- if (this.history.length === 0 || this.historyIndex + 1 > this.history.length) {
123
+ if (this.items.length === 0 || this.historyIndex + 1 > this.items.length) {
105
124
  return null;
106
125
  }
107
126
 
108
- if (this.historyIndex < this.history.length - 1) {
127
+ if (this.historyIndex < this.items.length - 1) {
109
128
  this.historyIndex += 1;
110
129
  } else {
111
130
  return null;
112
131
  }
113
132
 
114
- return this.history[this.historyIndex] || null;
133
+ return this.items[this.historyIndex] || null;
115
134
  }
116
135
  }