@expensify/react-native-live-markdown 0.1.116 → 0.1.118

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 (90) hide show
  1. package/lib/commonjs/MarkdownTextInput.js +2 -4
  2. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  3. package/lib/commonjs/MarkdownTextInput.web.js +105 -149
  4. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  5. package/lib/commonjs/styleUtils.js.map +1 -1
  6. package/lib/commonjs/web/utils/blockUtils.js +77 -0
  7. package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
  8. package/lib/commonjs/web/utils/browserUtils.js +18 -0
  9. package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
  10. package/lib/commonjs/web/utils/cursorUtils.js +111 -0
  11. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
  12. package/lib/commonjs/web/utils/inputUtils.js +108 -0
  13. package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
  14. package/lib/commonjs/web/utils/parserUtils.js +266 -0
  15. package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
  16. package/lib/commonjs/web/utils/treeUtils.js +85 -0
  17. package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
  18. package/lib/commonjs/web/utils/webStyleUtils.js +51 -0
  19. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
  20. package/lib/module/MarkdownTextInput.js +2 -2
  21. package/lib/module/MarkdownTextInput.js.map +1 -1
  22. package/lib/module/MarkdownTextInput.web.js +101 -145
  23. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  24. package/lib/module/styleUtils.js.map +1 -1
  25. package/lib/module/web/utils/blockUtils.js +72 -0
  26. package/lib/module/web/utils/blockUtils.js.map +1 -0
  27. package/lib/module/web/utils/browserUtils.js +12 -0
  28. package/lib/module/web/utils/browserUtils.js.map +1 -0
  29. package/lib/module/web/utils/cursorUtils.js +102 -0
  30. package/lib/module/web/utils/cursorUtils.js.map +1 -0
  31. package/lib/module/web/utils/inputUtils.js +99 -0
  32. package/lib/module/web/utils/inputUtils.js.map +1 -0
  33. package/lib/module/web/utils/parserUtils.js +260 -0
  34. package/lib/module/web/utils/parserUtils.js.map +1 -0
  35. package/lib/module/web/utils/treeUtils.js +78 -0
  36. package/lib/module/web/utils/treeUtils.js.map +1 -0
  37. package/lib/module/web/utils/webStyleUtils.js +45 -0
  38. package/lib/module/web/utils/webStyleUtils.js.map +1 -0
  39. package/lib/typescript/src/MarkdownTextInput.d.ts +1 -2
  40. package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
  41. package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -3
  42. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  43. package/lib/typescript/src/styleUtils.d.ts +1 -2
  44. package/lib/typescript/src/styleUtils.d.ts.map +1 -1
  45. package/lib/typescript/src/web/utils/blockUtils.d.ts +5 -0
  46. package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -0
  47. package/lib/typescript/src/web/utils/browserUtils.d.ts +12 -0
  48. package/lib/typescript/src/web/utils/browserUtils.d.ts.map +1 -0
  49. package/lib/typescript/src/web/utils/cursorUtils.d.ts +12 -0
  50. package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -0
  51. package/lib/typescript/src/web/utils/inputUtils.d.ts +9 -0
  52. package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -0
  53. package/lib/typescript/src/web/utils/parserUtils.d.ts +22 -0
  54. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -0
  55. package/lib/typescript/src/web/utils/treeUtils.d.ts +17 -0
  56. package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -0
  57. package/lib/typescript/src/web/utils/webStyleUtils.d.ts +6 -0
  58. package/lib/typescript/src/web/utils/webStyleUtils.d.ts.map +1 -0
  59. package/package.json +5 -2
  60. package/src/MarkdownTextInput.tsx +4 -7
  61. package/src/MarkdownTextInput.web.tsx +142 -171
  62. package/src/styleUtils.ts +1 -3
  63. package/src/web/utils/blockUtils.ts +72 -0
  64. package/src/web/utils/browserUtils.ts +13 -0
  65. package/src/web/utils/cursorUtils.ts +116 -0
  66. package/src/web/utils/inputUtils.ts +106 -0
  67. package/src/web/utils/parserUtils.ts +302 -0
  68. package/src/web/utils/treeUtils.ts +103 -0
  69. package/src/web/utils/webStyleUtils.ts +54 -0
  70. package/lib/commonjs/web/browserUtils.js +0 -16
  71. package/lib/commonjs/web/browserUtils.js.map +0 -1
  72. package/lib/commonjs/web/cursorUtils.js +0 -167
  73. package/lib/commonjs/web/cursorUtils.js.map +0 -1
  74. package/lib/commonjs/web/parserUtils.js +0 -214
  75. package/lib/commonjs/web/parserUtils.js.map +0 -1
  76. package/lib/module/web/browserUtils.js +0 -11
  77. package/lib/module/web/browserUtils.js.map +0 -1
  78. package/lib/module/web/cursorUtils.js +0 -154
  79. package/lib/module/web/cursorUtils.js.map +0 -1
  80. package/lib/module/web/parserUtils.js +0 -206
  81. package/lib/module/web/parserUtils.js.map +0 -1
  82. package/lib/typescript/src/web/browserUtils.d.ts +0 -10
  83. package/lib/typescript/src/web/browserUtils.d.ts.map +0 -1
  84. package/lib/typescript/src/web/cursorUtils.d.ts +0 -12
  85. package/lib/typescript/src/web/cursorUtils.d.ts.map +0 -1
  86. package/lib/typescript/src/web/parserUtils.d.ts +0 -17
  87. package/lib/typescript/src/web/parserUtils.d.ts.map +0 -1
  88. package/src/web/browserUtils.ts +0 -11
  89. package/src/web/cursorUtils.ts +0 -177
  90. package/src/web/parserUtils.ts +0 -228
@@ -11,48 +11,21 @@ import type {
11
11
  TextInputContentSizeChangeEventData,
12
12
  } from 'react-native';
13
13
  import React, {useEffect, useRef, useCallback, useMemo, useLayoutEffect} from 'react';
14
- import type {CSSProperties, MutableRefObject, ReactEventHandler, FocusEventHandler, MouseEvent, KeyboardEvent, SyntheticEvent} from 'react';
14
+ import type {CSSProperties, MutableRefObject, ReactEventHandler, FocusEventHandler, MouseEvent, KeyboardEvent, SyntheticEvent, ClipboardEventHandler} from 'react';
15
15
  import {StyleSheet} from 'react-native';
16
- import * as ParseUtils from './web/parserUtils';
17
- import * as CursorUtils from './web/cursorUtils';
18
- import * as StyleUtils from './styleUtils';
19
- import type * as MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent';
20
- import './web/MarkdownTextInput.css';
16
+ import {updateInputStructure} from './web/utils/parserUtils';
21
17
  import InputHistory from './web/InputHistory';
18
+ import type {TreeNode} from './web/utils/treeUtils';
19
+ import {getCurrentCursorPosition, removeSelection, setCursorPosition} from './web/utils/cursorUtils';
20
+ import './web/MarkdownTextInput.css';
21
+ import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent';
22
+ import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils';
23
+ import {parseToReactDOMStyle, processMarkdownStyle} from './web/utils/webStyleUtils';
22
24
 
23
25
  require('../parser/react-native-live-markdown-parser.js');
24
26
 
25
27
  const useClientEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
26
28
 
27
- let createReactDOMStyle: (style: any) => any;
28
- try {
29
- createReactDOMStyle =
30
- // eslint-disable-next-line @typescript-eslint/no-var-requires
31
- require('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle').default;
32
- } catch (e) {
33
- throw new Error('[react-native-live-markdown] Function `createReactDOMStyle` from react-native-web not found. Please make sure that you are using React Native Web 0.18 or newer.');
34
- }
35
-
36
- let preprocessStyle: (style: any) => any;
37
- try {
38
- preprocessStyle =
39
- // eslint-disable-next-line @typescript-eslint/no-var-requires
40
- require('react-native-web/dist/exports/StyleSheet/preprocess').default;
41
- } catch (e) {
42
- throw new Error('[react-native-live-markdown] Function `preprocessStyle` from react-native-web not found.');
43
- }
44
-
45
- let dangerousStyleValue: (name: string, value: any, isCustomProperty: boolean) => any;
46
- try {
47
- dangerousStyleValue =
48
- // eslint-disable-next-line @typescript-eslint/no-var-requires
49
- require('react-native-web/dist/modules/setValueForStyles/dangerousStyleValue').default;
50
- } catch (e) {
51
- throw new Error('[react-native-live-markdown] Function `dangerousStyleValue` from react-native-web not found.');
52
- }
53
-
54
- type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponent.MarkdownStyle;
55
-
56
29
  interface MarkdownTextInputProps extends TextInputProps {
57
30
  markdownStyle?: MarkdownStyle;
58
31
  onClick?: (e: MouseEvent<HTMLDivElement>) => void;
@@ -81,62 +54,14 @@ type ParseTextResult = {
81
54
 
82
55
  let focusTimeout: NodeJS.Timeout | null = null;
83
56
 
84
- // Removes one '\n' from the end of the string that were added by contentEditable div
85
- function normalizeValue(value: string) {
86
- return value.replace(/\n$/, '');
87
- }
88
- // Adds one '\n' at the end of the string if it's missing
89
- function denormalizeValue(value: string) {
90
- return value.endsWith('\n') ? `${value}\n` : value;
91
- }
92
-
93
- // If an Input Method Editor is processing key input, the 'keyCode' is 229.
94
- // https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
95
- function isEventComposing(nativeEvent: globalThis.KeyboardEvent) {
96
- return nativeEvent.isComposing || nativeEvent.keyCode === 229;
97
- }
57
+ type MarkdownTextInputElement = HTMLDivElement &
58
+ HTMLInputElement & {
59
+ tree: TreeNode;
60
+ };
98
61
 
99
- const ZERO_WIDTH_SPACE = '\u200B';
100
-
101
- function getPlaceholderValue(placeholder: string | undefined) {
102
- if (!placeholder) {
103
- return ZERO_WIDTH_SPACE;
104
- }
105
- return placeholder.length ? placeholder : ZERO_WIDTH_SPACE;
106
- }
107
-
108
- function processUnitsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
109
- const output = JSON.parse(JSON.stringify(input));
110
-
111
- Object.keys(output).forEach((key) => {
112
- const obj = output[key];
113
- Object.keys(obj).forEach((prop) => {
114
- obj[prop] = dangerousStyleValue(prop, obj[prop], false);
115
- });
116
- });
117
-
118
- return output as MarkdownStyle;
119
- }
120
-
121
- function processMarkdownStyle(input: MarkdownStyle | undefined): MarkdownStyle {
122
- return processUnitsInMarkdownStyle(StyleUtils.mergeMarkdownStyleWithDefault(input));
123
- }
124
-
125
- function getElementHeight(node: HTMLDivElement, styles: CSSProperties, numberOfLines: number | undefined) {
126
- if (numberOfLines) {
127
- const tempElement = document.createElement('div');
128
- tempElement.setAttribute('contenteditable', 'true');
129
- Object.assign(tempElement.style, styles);
130
- tempElement.innerText = Array(numberOfLines).fill('A').join('\n');
131
- if (node.parentElement) {
132
- node.parentElement.appendChild(tempElement);
133
- const height = tempElement.clientHeight;
134
- node.parentElement.removeChild(tempElement);
135
- return height ? `${height}px` : 'auto';
136
- }
137
- }
138
- return styles.height ? `${styles.height}px` : 'auto';
139
- }
62
+ type HTMLMarkdownElement = HTMLElement & {
63
+ value: string;
64
+ };
140
65
 
141
66
  const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
142
67
  (
@@ -176,13 +101,13 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
176
101
  ref,
177
102
  ) => {
178
103
  const compositionRef = useRef<boolean>(false);
179
- const pasteRef = useRef<boolean>(false);
180
- const divRef = useRef<HTMLDivElement | null>(null);
104
+ const divRef = useRef<MarkdownTextInputElement | null>(null);
181
105
  const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
182
106
  const contentSelection = useRef<Selection | null>(null);
183
107
  const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
184
108
  const history = useRef<InputHistory>();
185
- const dimensions = React.useRef<Dimensions | null>(null);
109
+ const dimensions = useRef<Dimensions | null>(null);
110
+ const pasteContent = useRef<string | null>(null);
186
111
 
187
112
  if (!history.current) {
188
113
  history.current = new InputHistory(100, 150, value || '');
@@ -195,7 +120,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
195
120
 
196
121
  const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
197
122
  if (divRef.current) {
198
- const text = normalizeValue(divRef.current.innerText || '');
123
+ const text = divRef.current.value;
199
124
  if (e.target) {
200
125
  // TODO: change the logic here so every event have value property
201
126
  (e.target as unknown as HTMLInputElement).value = text;
@@ -208,14 +133,26 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
208
133
  }, []);
209
134
 
210
135
  const parseText = useCallback(
211
- (target: HTMLDivElement, text: string | null, customMarkdownStyles: MarkdownStyle, cursorPosition: number | null = null, shouldAddToHistory = true): ParseTextResult => {
136
+ (
137
+ target: MarkdownTextInputElement,
138
+ text: string | null,
139
+ customMarkdownStyles: MarkdownStyle,
140
+ cursorPosition: number | null = null,
141
+ shouldAddToHistory = true,
142
+ shouldForceDOMUpdate = false,
143
+ ): ParseTextResult => {
144
+ if (!divRef.current) {
145
+ return {text: text || '', cursorPosition: null};
146
+ }
147
+
212
148
  if (text === null) {
213
- return {text: target.innerText, cursorPosition: null};
149
+ return {text: divRef.current.value, cursorPosition: null};
214
150
  }
215
- const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline);
151
+ const parsedText = updateInputStructure(target, text, cursorPosition, customMarkdownStyles, !multiline, shouldForceDOMUpdate);
152
+ divRef.current.value = parsedText.text;
153
+
216
154
  if (history.current && shouldAddToHistory) {
217
- // 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
218
- history.current.throttledAdd(normalizeValue(parsedText.text), parsedText.cursorPosition);
155
+ history.current.throttledAdd(parsedText.text, parsedText.cursorPosition);
219
156
  }
220
157
 
221
158
  return parsedText;
@@ -226,7 +163,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
226
163
  const processedMarkdownStyle = useMemo(() => {
227
164
  const newMarkdownStyle = processMarkdownStyle(markdownStyle);
228
165
  if (divRef.current) {
229
- parseText(divRef.current, divRef.current.innerText, newMarkdownStyle, null, false);
166
+ parseText(divRef.current, divRef.current.value, newMarkdownStyle, null, false);
230
167
  }
231
168
  return newMarkdownStyle;
232
169
  }, [markdownStyle, parseText]);
@@ -239,13 +176,13 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
239
176
  caretColor: (flattenedStyle as TextStyle).color || 'black',
240
177
  },
241
178
  disabled && styles.disabledInputStyles,
242
- createReactDOMStyle(preprocessStyle(flattenedStyle)),
179
+ parseToReactDOMStyle(flattenedStyle),
243
180
  ]) as CSSProperties,
244
181
  [flattenedStyle, disabled],
245
182
  );
246
183
 
247
184
  const undo = useCallback(
248
- (target: HTMLDivElement): ParseTextResult => {
185
+ (target: MarkdownTextInputElement): ParseTextResult => {
249
186
  if (!history.current) {
250
187
  return {
251
188
  text: '',
@@ -253,14 +190,14 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
253
190
  };
254
191
  }
255
192
  const item = history.current.undo();
256
- const undoValue = item ? denormalizeValue(item.text) : null;
193
+ const undoValue = item ? item.text : null;
257
194
  return parseText(target, undoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false);
258
195
  },
259
196
  [parseText, processedMarkdownStyle],
260
197
  );
261
198
 
262
199
  const redo = useCallback(
263
- (target: HTMLDivElement): ParseTextResult => {
200
+ (target: MarkdownTextInputElement): ParseTextResult => {
264
201
  if (!history.current) {
265
202
  return {
266
203
  text: '',
@@ -268,20 +205,12 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
268
205
  };
269
206
  }
270
207
  const item = history.current.redo();
271
- const redoValue = item ? denormalizeValue(item.text) : null;
208
+ const redoValue = item ? item.text : null;
272
209
  return parseText(target, redoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false);
273
210
  },
274
211
  [parseText, processedMarkdownStyle],
275
212
  );
276
213
 
277
- // We have to process value property since contentEditable div adds one additional '\n' at the end of the text if we are entering new line
278
- const processedValue = useMemo(() => {
279
- if (value && value[value.length - 1] === '\n') {
280
- return `${value}\n`;
281
- }
282
- return value;
283
- }, [value]);
284
-
285
214
  // Placeholder text color logic
286
215
  const updateTextColor = useCallback(
287
216
  (node: HTMLDivElement, text: string) => {
@@ -315,7 +244,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
315
244
  if (!divRef.current) {
316
245
  return;
317
246
  }
318
- const newSelection = predefinedSelection || CursorUtils.getCurrentCursorPosition(divRef.current);
247
+ const newSelection = predefinedSelection || getCurrentCursorPosition(divRef.current);
319
248
 
320
249
  if (newSelection && (!contentSelection.current || contentSelection.current.start !== newSelection.start || contentSelection.current.end !== newSelection.end)) {
321
250
  updateRefSelectionVariables(newSelection);
@@ -349,20 +278,31 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
349
278
 
350
279
  const handleOnChangeText = useCallback(
351
280
  (e: SyntheticEvent<HTMLDivElement>) => {
352
- if (!divRef.current || !(e.target instanceof HTMLElement)) {
281
+ if (!divRef.current || !(e.target instanceof HTMLElement) || !contentSelection.current) {
353
282
  return;
354
283
  }
284
+ const nativeEvent = e.nativeEvent as MarkdownNativeEvent;
285
+ const inputType = nativeEvent.inputType;
286
+
287
+ updateTextColor(divRef.current, e.target.textContent ?? '');
288
+ const previousText = divRef.current.value;
289
+ const parsedText = normalizeValue(inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement));
290
+
291
+ if (pasteContent.current) {
292
+ pasteContent.current = null;
293
+ }
294
+
355
295
  const prevSelection = contentSelection.current ?? {start: 0, end: 0};
356
- const prevTextLength = CursorUtils.getPrevTextLength() ?? 0;
357
- const changedText = e.target.innerText;
296
+ const newCursorPosition = Math.max(Math.max(contentSelection.current.end, 0) + (parsedText.length - previousText.length), 0);
297
+
358
298
  if (compositionRef.current) {
359
- updateTextColor(divRef.current, changedText);
299
+ divRef.current.value = parsedText;
300
+ compositionRef.current = false;
301
+ contentSelection.current.end = newCursorPosition;
360
302
  return;
361
303
  }
362
304
 
363
305
  let newInputUpdate: ParseTextResult;
364
- const nativeEvent = e.nativeEvent as MarkdownNativeEvent;
365
- const inputType = nativeEvent.inputType;
366
306
  switch (inputType) {
367
307
  case 'historyUndo':
368
308
  newInputUpdate = undo(divRef.current);
@@ -370,26 +310,15 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
370
310
  case 'historyRedo':
371
311
  newInputUpdate = redo(divRef.current);
372
312
  break;
373
- case 'insertFromPaste':
374
- // 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
375
- if (changedText.length > 2 && changedText[changedText.length - 2] !== '\n' && changedText[changedText.length - 1] === '\n') {
376
- newInputUpdate = parseText(divRef.current, normalizeValue(changedText), processedMarkdownStyle);
377
- break;
378
- }
379
- newInputUpdate = parseText(divRef.current, changedText, processedMarkdownStyle);
380
- break;
381
313
  default:
382
- newInputUpdate = parseText(divRef.current, changedText, processedMarkdownStyle);
314
+ newInputUpdate = parseText(divRef.current, parsedText, processedMarkdownStyle, newCursorPosition, true, !inputType);
383
315
  }
384
-
385
316
  const {text, cursorPosition} = newInputUpdate;
386
- const normalizedText = normalizeValue(text);
387
-
388
- if (pasteRef?.current) {
389
- pasteRef.current = false;
390
- updateSelection(e);
391
- }
392
317
  updateTextColor(divRef.current, text);
318
+ updateSelection(e, {
319
+ start: cursorPosition ?? 0,
320
+ end: cursorPosition ?? 0,
321
+ });
393
322
 
394
323
  if (onChange) {
395
324
  const event = e as unknown as NativeSyntheticEvent<{
@@ -400,7 +329,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
400
329
  setEventProps(event);
401
330
 
402
331
  // The new text is between the prev start selection and the new end selection, can be empty
403
- const addedText = normalizedText.slice(prevSelection.start, cursorPosition ?? 0);
332
+ const addedText = text.slice(prevSelection.start, cursorPosition ?? 0);
404
333
  // The length of the text that replaced the before text
405
334
  const count = addedText.length;
406
335
  // The start index of the replacement operation
@@ -411,7 +340,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
411
340
  let before = prevSelectionRange;
412
341
  if (prevSelectionRange === 0 && (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward')) {
413
342
  // 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
414
- before = prevTextLength - normalizedText.length;
343
+ before = previousText.length - text.length;
415
344
  }
416
345
 
417
346
  if (inputType === 'deleteContentBackward') {
@@ -429,12 +358,33 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
429
358
  }
430
359
 
431
360
  if (onChangeText) {
432
- onChangeText(normalizedText);
361
+ onChangeText(text);
433
362
  }
434
363
 
435
364
  handleContentSizeChange();
436
365
  },
437
- [updateTextColor, handleContentSizeChange, onChange, onChangeText, undo, redo, parseText, processedMarkdownStyle, updateSelection, setEventProps],
366
+ [updateTextColor, updateSelection, onChange, onChangeText, handleContentSizeChange, undo, redo, parseText, processedMarkdownStyle, setEventProps],
367
+ );
368
+
369
+ const insertText = useCallback(
370
+ (e: SyntheticEvent<HTMLDivElement, Event>, text: string) => {
371
+ if (!contentSelection.current || !divRef.current) {
372
+ return;
373
+ }
374
+
375
+ const previousText = divRef.current.value;
376
+ const newText = `${divRef.current.value.substring(0, contentSelection.current.start)}${text}${divRef.current.value.substring(contentSelection.current.end)}`;
377
+ if (previousText === newText) {
378
+ document.execCommand('delete');
379
+ }
380
+
381
+ pasteContent.current = newText;
382
+ (e.nativeEvent as MarkdownNativeEvent).inputType = 'pasteText';
383
+
384
+ handleOnChangeText(e);
385
+ updateSelection(e);
386
+ },
387
+ [handleOnChangeText, updateSelection],
438
388
  );
439
389
 
440
390
  const handleKeyPress = useCallback(
@@ -446,7 +396,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
446
396
  const hostNode = e.target;
447
397
  e.stopPropagation();
448
398
 
449
- if (e.key === 'z' && e.metaKey) {
399
+ if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
450
400
  e.preventDefault();
451
401
  const nativeEvent = e.nativeEvent as unknown as MarkdownNativeEvent;
452
402
  if (e.shiftKey) {
@@ -486,16 +436,14 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
486
436
  } else if (multiline) {
487
437
  // We need to change normal behavior of "Enter" key to insert a line breaks, to prevent wrapping contentEditable text in <div> tags.
488
438
  // Thanks to that in every situation we have proper amount of new lines in our parsed text. Without it pressing enter in empty lines will add 2 more new lines.
489
- document.execCommand('insertLineBreak');
490
- CursorUtils.scrollCursorIntoView(divRef.current as HTMLInputElement);
439
+ insertText(e, '\n');
491
440
  }
492
-
493
441
  if (!e.shiftKey && ((shouldBlurOnSubmit && hostNode !== null) || !multiline)) {
494
442
  setTimeout(() => divRef.current && divRef.current.blur(), 0);
495
443
  }
496
444
  }
497
445
  },
498
- [multiline, blurOnSubmit, setEventProps, onKeyPress, updateSelection, handleOnChangeText, onSubmitEditing],
446
+ [multiline, blurOnSubmit, setEventProps, onKeyPress, updateSelection, handleOnChangeText, onSubmitEditing, insertText],
499
447
  );
500
448
 
501
449
  const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
@@ -506,10 +454,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
506
454
  setEventProps(e);
507
455
  if (divRef.current) {
508
456
  if (contentSelection.current) {
509
- CursorUtils.setCursorPosition(divRef.current, contentSelection.current.start, contentSelection.current.end);
457
+ setCursorPosition(divRef.current, contentSelection.current.start, contentSelection.current.end);
510
458
  } else {
511
- const valueLength = value ? value.length : divRef.current.innerText.length;
512
- CursorUtils.setCursorPosition(divRef.current, valueLength, null);
459
+ const valueLength = value ? value.length : divRef.current.value.length;
460
+ setCursorPosition(divRef.current, valueLength, null);
513
461
  }
514
462
  updateSelection(event, contentSelection.current);
515
463
  }
@@ -521,7 +469,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
521
469
 
522
470
  if (hostNode !== null) {
523
471
  if (clearTextOnFocus && divRef.current) {
524
- divRef.current.innerText = '';
472
+ divRef.current.textContent = '';
525
473
  }
526
474
  if (selectTextOnFocus) {
527
475
  // Safari requires selection to occur in a setTimeout
@@ -543,7 +491,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
543
491
  const handleBlur: FocusEventHandler<HTMLDivElement> = useCallback(
544
492
  (event) => {
545
493
  const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
546
- CursorUtils.removeSelection();
494
+ removeSelection();
547
495
  currentlyFocusedField.current = null;
548
496
  if (onBlur) {
549
497
  setEventProps(e);
@@ -559,24 +507,47 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
559
507
  if (!onClick || !divRef.current) {
560
508
  return;
561
509
  }
562
- (e.target as HTMLInputElement).value = normalizeValue(divRef.current.innerText || '');
510
+ (e.target as HTMLInputElement).value = divRef.current.value;
563
511
  onClick(e);
564
512
  },
565
513
  [onClick, updateSelection],
566
514
  );
567
515
 
568
- const handlePaste = useCallback((e) => {
569
- pasteRef.current = true;
570
- if (e.isDefaultPrevented()) {
516
+ const handleCopy: ClipboardEventHandler<HTMLDivElement> = useCallback((e) => {
517
+ if (!divRef.current || !contentSelection.current) {
571
518
  return;
572
519
  }
573
-
574
520
  e.preventDefault();
575
- const clipboardData = e.clipboardData;
576
- const text = clipboardData.getData('text/plain');
577
- document.execCommand('insertText', false, text);
521
+ const text = divRef.current?.value.substring(contentSelection.current.start, contentSelection.current.end);
522
+ e.clipboardData.setData('text/plain', text ?? '');
578
523
  }, []);
579
524
 
525
+ const handleCut = useCallback(
526
+ (e) => {
527
+ if (!divRef.current || !contentSelection.current) {
528
+ return;
529
+ }
530
+ handleCopy(e);
531
+ if (contentSelection.current.start !== contentSelection.current.end) {
532
+ document.execCommand('delete');
533
+ }
534
+ },
535
+ [handleCopy],
536
+ );
537
+
538
+ const handlePaste = useCallback(
539
+ (e) => {
540
+ if (e.isDefaultPrevented() || !divRef.current || !contentSelection.current) {
541
+ return;
542
+ }
543
+ e.preventDefault();
544
+ const clipboardData = e.clipboardData;
545
+ const text = clipboardData.getData('text/plain');
546
+ insertText(e, text);
547
+ },
548
+ [insertText],
549
+ );
550
+
580
551
  const startComposition = useCallback(() => {
581
552
  compositionRef.current = true;
582
553
  }, []);
@@ -594,13 +565,13 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
594
565
  if (r) {
595
566
  (r as unknown as TextInput).isFocused = () => document.activeElement === r;
596
567
  (r as unknown as TextInput).clear = () => {
597
- r.innerText = '';
568
+ r.textContent = '';
598
569
  updateTextColor(r, '');
599
570
  };
600
571
 
601
572
  if (value === '' || value === undefined) {
602
573
  // update to placeholder color when value is empty
603
- updateTextColor(r, r.innerText);
574
+ updateTextColor(r, r.textContent ?? '');
604
575
  }
605
576
  }
606
577
 
@@ -612,26 +583,25 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
612
583
  (ref as (elementRef: HTMLDivElement | null) => void)(r);
613
584
  }
614
585
  }
615
- divRef.current = r;
586
+ divRef.current = r as MarkdownTextInputElement;
616
587
  };
617
588
 
618
589
  useClientEffect(
619
590
  function parseAndStyleValue() {
620
- if (!divRef.current || processedValue === divRef.current.innerText) {
591
+ if (!divRef.current || value === divRef.current.value) {
621
592
  return;
622
593
  }
623
594
 
624
595
  if (value === undefined) {
625
- parseText(divRef.current, divRef.current.innerText, processedMarkdownStyle);
596
+ parseText(divRef.current, divRef.current.value, processedMarkdownStyle);
626
597
  return;
627
598
  }
628
-
629
- const text = processedValue !== undefined ? processedValue : '';
630
-
631
- parseText(divRef.current, text, processedMarkdownStyle, text.length);
599
+ const normalizedValue = normalizeValue(value);
600
+ divRef.current.value = normalizedValue;
601
+ parseText(divRef.current, normalizedValue, processedMarkdownStyle);
632
602
  updateTextColor(divRef.current, value);
633
603
  },
634
- [multiline, processedMarkdownStyle, processedValue],
604
+ [multiline, processedMarkdownStyle, value],
635
605
  );
636
606
 
637
607
  useClientEffect(
@@ -666,11 +636,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
666
636
  if (!divRef.current || !selection || (contentSelection.current && selection.start === contentSelection.current.start && selection.end === contentSelection.current.end)) {
667
637
  return;
668
638
  }
669
-
670
639
  const newSelection: Selection = {start: selection.start, end: selection.end ?? selection.start};
671
640
  contentSelection.current = newSelection;
672
641
  updateRefSelectionVariables(newSelection);
673
- CursorUtils.setCursorPosition(divRef.current, newSelection.start, newSelection.end);
642
+ setCursorPosition(divRef.current, newSelection.start, newSelection.end);
674
643
  }, [selection, updateRefSelectionVariables]);
675
644
 
676
645
  return (
@@ -696,6 +665,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
696
665
  onClick={handleClick}
697
666
  onFocus={handleFocus}
698
667
  onBlur={handleBlur}
668
+ onCopy={handleCopy}
669
+ onCut={handleCut}
699
670
  onPaste={handlePaste}
700
671
  placeholder={heightSafePlaceholder}
701
672
  spellCheck={spellCheck}
@@ -727,4 +698,4 @@ const styles = StyleSheet.create({
727
698
 
728
699
  export default MarkdownTextInput;
729
700
 
730
- export type {MarkdownTextInputProps};
701
+ export type {MarkdownTextInputProps, MarkdownTextInputElement, HTMLMarkdownElement};
package/src/styleUtils.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import {Platform} from 'react-native';
2
- import type * as MarkdownTextInputDecoractorView from './MarkdownTextInputDecoratorViewNativeComponent';
3
-
4
- type MarkdownStyle = MarkdownTextInputDecoractorView.MarkdownStyle;
2
+ import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent';
5
3
 
6
4
  type PartialMarkdownStyle = Partial<{
7
5
  [K in keyof MarkdownStyle]: Partial<MarkdownStyle[K]>;
@@ -0,0 +1,72 @@
1
+ import type {PartialMarkdownStyle} from '../../styleUtils';
2
+ import type {NodeType} from './treeUtils';
3
+
4
+ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownStyle: PartialMarkdownStyle) {
5
+ const node = targetElement;
6
+ switch (type) {
7
+ case 'line':
8
+ Object.assign(node.style, {
9
+ display: 'block',
10
+ margin: '0',
11
+ padding: '0',
12
+ });
13
+ break;
14
+ case 'syntax':
15
+ Object.assign(node.style, markdownStyle.syntax);
16
+ break;
17
+ case 'bold':
18
+ node.style.fontWeight = 'bold';
19
+ break;
20
+ case 'italic':
21
+ node.style.fontStyle = 'italic';
22
+ break;
23
+ case 'strikethrough':
24
+ node.style.textDecoration = 'line-through';
25
+ break;
26
+ case 'emoji':
27
+ Object.assign(node.style, {...markdownStyle.emoji, verticalAlign: 'middle'});
28
+ break;
29
+ case 'mention-here':
30
+ Object.assign(node.style, markdownStyle.mentionHere);
31
+ break;
32
+ case 'mention-user':
33
+ Object.assign(node.style, markdownStyle.mentionUser);
34
+ break;
35
+ case 'mention-report':
36
+ Object.assign(node.style, markdownStyle.mentionReport);
37
+ break;
38
+ case 'link':
39
+ Object.assign(node.style, {
40
+ ...markdownStyle.link,
41
+ textDecoration: 'underline',
42
+ });
43
+ break;
44
+ case 'code':
45
+ Object.assign(node.style, markdownStyle.code);
46
+ break;
47
+ case 'pre':
48
+ Object.assign(node.style, markdownStyle.pre);
49
+ break;
50
+
51
+ case 'blockquote':
52
+ Object.assign(node.style, {
53
+ ...markdownStyle.blockquote,
54
+ borderLeftStyle: 'solid',
55
+ display: 'inline-block',
56
+ maxWidth: '100%',
57
+ boxSizing: 'border-box',
58
+ });
59
+ break;
60
+ case 'h1':
61
+ Object.assign(node.style, {
62
+ ...markdownStyle.h1,
63
+ fontWeight: 'bold',
64
+ });
65
+ break;
66
+ default:
67
+ break;
68
+ }
69
+ }
70
+
71
+ // eslint-disable-next-line import/prefer-default-export
72
+ export {addStyleToBlock};
@@ -0,0 +1,13 @@
1
+ const BrowserUtils = {
2
+ isFirefox: navigator.userAgent.toLowerCase().includes('firefox'),
3
+ isChromium: 'chrome' in window,
4
+
5
+ /**
6
+ * Whether the platform is a mobile browser.
7
+ * Copied from Expensify App https://github.com/Expensify/App/blob/90dee7accae79c49debf30354c160cab6c52c423/src/libs/Browser/index.website.ts#L41
8
+ *
9
+ */
10
+ isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Silk|Opera Mini/i.test(navigator.userAgent),
11
+ };
12
+
13
+ export default BrowserUtils;