@expensify/react-native-live-markdown 0.1.3 → 0.1.4

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 (53) hide show
  1. package/lib/commonjs/MarkdownTextInput.js +5 -55
  2. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  3. package/lib/commonjs/MarkdownTextInput.web.js +399 -9
  4. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  5. package/lib/commonjs/react-native-web.d.js +2 -0
  6. package/lib/commonjs/react-native-web.d.js.map +1 -0
  7. package/lib/commonjs/styleUtils.js +61 -0
  8. package/lib/commonjs/styleUtils.js.map +1 -0
  9. package/lib/commonjs/web/InputHistory.js +92 -0
  10. package/lib/commonjs/web/InputHistory.js.map +1 -0
  11. package/lib/commonjs/web/MarkdownTextInput.css +24 -0
  12. package/lib/commonjs/web/cursorUtils.js +90 -0
  13. package/lib/commonjs/web/cursorUtils.js.map +1 -0
  14. package/lib/commonjs/web/parserUtils.js +170 -0
  15. package/lib/commonjs/web/parserUtils.js.map +1 -0
  16. package/lib/module/MarkdownTextInput.js +3 -55
  17. package/lib/module/MarkdownTextInput.js.map +1 -1
  18. package/lib/module/MarkdownTextInput.web.js +399 -12
  19. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  20. package/lib/module/react-native-web.d.js +2 -0
  21. package/lib/module/react-native-web.d.js.map +1 -0
  22. package/lib/module/styleUtils.js +56 -0
  23. package/lib/module/styleUtils.js.map +1 -0
  24. package/lib/module/web/InputHistory.js +85 -0
  25. package/lib/module/web/InputHistory.js.map +1 -0
  26. package/lib/module/web/MarkdownTextInput.css +24 -0
  27. package/lib/module/web/cursorUtils.js +83 -0
  28. package/lib/module/web/cursorUtils.js.map +1 -0
  29. package/lib/module/web/parserUtils.js +162 -0
  30. package/lib/module/web/parserUtils.js.map +1 -0
  31. package/lib/parser/react-native-live-markdown-parser.js +34 -0
  32. package/lib/typescript/src/MarkdownTextInput.d.ts +3 -6
  33. package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
  34. package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -2
  35. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  36. package/lib/typescript/src/styleUtils.d.ts +9 -0
  37. package/lib/typescript/src/styleUtils.d.ts.map +1 -0
  38. package/lib/typescript/src/web/InputHistory.d.ts +24 -0
  39. package/lib/typescript/src/web/InputHistory.d.ts.map +1 -0
  40. package/lib/typescript/src/web/cursorUtils.d.ts +8 -0
  41. package/lib/typescript/src/web/cursorUtils.d.ts.map +1 -0
  42. package/lib/typescript/src/web/parserUtils.d.ts +16 -0
  43. package/lib/typescript/src/web/parserUtils.d.ts.map +1 -0
  44. package/package.json +6 -3
  45. package/parser/react-native-live-markdown-parser.js +7 -7
  46. package/src/MarkdownTextInput.tsx +9 -69
  47. package/src/MarkdownTextInput.web.tsx +516 -16
  48. package/src/react-native-web.d.ts +1 -0
  49. package/src/styleUtils.ts +70 -0
  50. package/src/web/InputHistory.ts +114 -0
  51. package/src/web/MarkdownTextInput.css +24 -0
  52. package/src/web/cursorUtils.ts +87 -0
  53. package/src/web/parserUtils.ts +194 -0
@@ -1,72 +1,16 @@
1
+ import {StyleSheet, TextInput, processColor} from 'react-native';
1
2
  import React from 'react';
2
- import {Platform, StyleSheet, TextInput, processColor} from 'react-native';
3
3
  import type {TextInputProps} from 'react-native';
4
-
5
- import type * as MarkdownTextInputDecoractorView from './MarkdownTextInputDecoratorViewNativeComponent';
6
4
  import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent';
5
+ import type * as MarkdownTextInputDecoratorViewNativeComponentTypes from './MarkdownTextInputDecoratorViewNativeComponent';
6
+ import * as StyleUtils from './styleUtils';
7
+ import type * as StyleUtilsTypes from './styleUtils';
7
8
 
8
- type MarkdownStyle = MarkdownTextInputDecoractorView.MarkdownStyle;
9
-
10
- const FONT_FAMILY_MONOSPACE = Platform.select({
11
- ios: 'Courier',
12
- default: 'monospace',
13
- });
14
-
15
- function makeDefaultMarkdownStyle(): MarkdownStyle {
16
- return {
17
- syntax: {
18
- color: 'gray',
19
- },
20
- link: {
21
- color: 'blue',
22
- },
23
- h1: {
24
- fontSize: 25,
25
- },
26
- blockquote: {
27
- borderColor: 'gray',
28
- borderWidth: 6,
29
- marginLeft: 6,
30
- paddingLeft: 6,
31
- },
32
- code: {
33
- fontFamily: FONT_FAMILY_MONOSPACE,
34
- color: 'black',
35
- backgroundColor: 'lightgray',
36
- },
37
- pre: {
38
- fontFamily: FONT_FAMILY_MONOSPACE,
39
- color: 'black',
40
- backgroundColor: 'lightgray',
41
- },
42
- mentionHere: {
43
- color: 'green',
44
- backgroundColor: 'lime',
45
- },
46
- mentionUser: {
47
- color: 'blue',
48
- backgroundColor: 'cyan',
49
- },
50
- };
51
- }
9
+ type PartialMarkdownStyle = StyleUtilsTypes.PartialMarkdownStyle;
10
+ type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponentTypes.MarkdownStyle;
52
11
 
53
- type PartialMarkdownStyle = Partial<{
54
- [K in keyof MarkdownStyle]: Partial<MarkdownStyle[K]>;
55
- }>;
56
-
57
- function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined): MarkdownStyle {
58
- const output = makeDefaultMarkdownStyle();
59
-
60
- if (input !== undefined) {
61
- Object.keys(input).forEach((key) => {
62
- if (!(key in output)) {
63
- return;
64
- }
65
- Object.assign(output[key as keyof MarkdownStyle], input[key as keyof MarkdownStyle]);
66
- });
67
- }
68
-
69
- return output;
12
+ interface MarkdownTextInputProps extends TextInputProps {
13
+ markdownStyle?: PartialMarkdownStyle;
70
14
  }
71
15
 
72
16
  function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
@@ -87,11 +31,7 @@ function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
87
31
  }
88
32
 
89
33
  function processMarkdownStyle(input: PartialMarkdownStyle | undefined): MarkdownStyle {
90
- return processColorsInMarkdownStyle(mergeMarkdownStyleWithDefault(input));
91
- }
92
-
93
- interface MarkdownTextInputProps extends TextInputProps {
94
- markdownStyle?: PartialMarkdownStyle;
34
+ return processColorsInMarkdownStyle(StyleUtils.mergeMarkdownStyleWithDefault(input));
95
35
  }
96
36
 
97
37
  const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>((props, ref) => {
@@ -1,22 +1,522 @@
1
- import {TextInput} from 'react-native';
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type {
3
+ TextInput,
4
+ TextInputSubmitEditingEventData,
5
+ TextStyle,
6
+ NativeSyntheticEvent,
7
+ TextInputSelectionChangeEventData,
8
+ TextInputProps,
9
+ TextInputKeyPressEventData,
10
+ TextInputFocusEventData,
11
+ } from 'react-native';
12
+ import React, {useEffect, useRef, useCallback, useMemo} from 'react';
13
+ import type {CSSProperties, MutableRefObject, ReactEventHandler, FocusEventHandler, MouseEvent, KeyboardEvent, SyntheticEvent} from 'react';
14
+ import {StyleSheet} from 'react-native';
15
+ import * as ParseUtils from './web/parserUtils';
16
+ import * as CursorUtils from './web/cursorUtils';
17
+ import * as StyleUtils from './styleUtils';
18
+ import type * as MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent';
19
+ import './web/MarkdownTextInput.css';
20
+ import InputHistory from './web/InputHistory';
2
21
 
3
- import React from 'react';
4
- import type {TextInputProps} from 'react-native';
22
+ require('../parser/react-native-live-markdown-parser.js');
23
+
24
+ let createReactDOMStyle: (style: any) => any;
25
+ try {
26
+ createReactDOMStyle =
27
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
28
+ require('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle').default;
29
+ } catch (e) {
30
+ 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.');
31
+ }
32
+
33
+ let preprocessStyle: (style: any) => any;
34
+ try {
35
+ preprocessStyle =
36
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
37
+ require('react-native-web/dist/exports/StyleSheet/preprocess').default;
38
+ } catch (e) {
39
+ throw new Error('[react-native-live-markdown] Function `preprocessStyle` from react-native-web not found.');
40
+ }
41
+
42
+ let dangerousStyleValue: (name: string, value: any, isCustomProperty: boolean) => any;
43
+ try {
44
+ dangerousStyleValue =
45
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
46
+ require('react-native-web/dist/modules/setValueForStyles/dangerousStyleValue').default;
47
+ } catch (e) {
48
+ throw new Error('[react-native-live-markdown] Function `dangerousStyleValue` from react-native-web not found.');
49
+ }
50
+
51
+ type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponent.MarkdownStyle;
5
52
 
6
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
7
53
  interface MarkdownTextInputProps extends TextInputProps {
8
- // nothing here
9
- }
10
-
11
- // eslint-disable-next-line arrow-body-style
12
- const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>((props, ref) => {
13
- // TODO: add web implementation here
14
- return (
15
- <TextInput
16
- {...props}
17
- ref={ref}
18
- />
19
- );
54
+ markdownStyle?: MarkdownStyle;
55
+ onClick?: (e: MouseEvent<HTMLDivElement>) => void;
56
+ dir?: string;
57
+ disabled?: boolean;
58
+ }
59
+
60
+ interface MarkdownNativeEvent extends Event {
61
+ inputType: string;
62
+ }
63
+
64
+ type Selection = {
65
+ start: number;
66
+ end: number;
67
+ };
68
+
69
+ let focusTimeout: NodeJS.Timeout | null = null;
70
+
71
+ // Removes one '\n' from the end of the string that were added by contentEditable div
72
+ function normalizeValue(value: string) {
73
+ return value.replace(/\n$/, '');
74
+ }
75
+
76
+ // If an Input Method Editor is processing key input, the 'keyCode' is 229.
77
+ // https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
78
+ function isEventComposing(nativeEvent: globalThis.KeyboardEvent) {
79
+ return nativeEvent.isComposing || nativeEvent.keyCode === 229;
80
+ }
81
+
82
+ const ZERO_WIDTH_SPACE = '\u200B';
83
+
84
+ function getPlaceholderValue(placeholder: string | undefined) {
85
+ if (!placeholder) {
86
+ return ZERO_WIDTH_SPACE;
87
+ }
88
+ return placeholder.length ? placeholder : ZERO_WIDTH_SPACE;
89
+ }
90
+
91
+ function processUnitsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
92
+ const output = JSON.parse(JSON.stringify(input));
93
+
94
+ Object.keys(output).forEach((key) => {
95
+ const obj = output[key];
96
+ Object.keys(obj).forEach((prop) => {
97
+ obj[prop] = dangerousStyleValue(prop, obj[prop], false);
98
+ });
99
+ });
100
+
101
+ return output as MarkdownStyle;
102
+ }
103
+
104
+ function processMarkdownStyle(input: MarkdownStyle | undefined): MarkdownStyle {
105
+ return processUnitsInMarkdownStyle(StyleUtils.mergeMarkdownStyleWithDefault(input));
106
+ }
107
+
108
+ function getElementHeight(node: HTMLDivElement, styles: CSSProperties, numberOfLines: number | undefined) {
109
+ if (numberOfLines) {
110
+ const tempElement = document.createElement('div');
111
+ tempElement.setAttribute('contenteditable', 'true');
112
+ Object.assign(tempElement.style, styles);
113
+ tempElement.innerText = Array(numberOfLines).fill('A').join('\n');
114
+ if (node.parentElement) {
115
+ node.parentElement.appendChild(tempElement);
116
+ const height = tempElement.clientHeight;
117
+ node.parentElement.removeChild(tempElement);
118
+ return `${height}px`;
119
+ }
120
+ }
121
+ return `${styles.height}px` || 'auto';
122
+ }
123
+
124
+ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
125
+ (
126
+ {
127
+ accessibilityLabel,
128
+ accessibilityLabelledBy,
129
+ accessibilityRole,
130
+ autoCapitalize = 'sentences',
131
+ autoCorrect = true,
132
+ blurOnSubmit = false,
133
+ clearTextOnFocus,
134
+ dir = 'auto',
135
+ disabled = false,
136
+ numberOfLines,
137
+ multiline = false,
138
+ markdownStyle,
139
+ onBlur,
140
+ onChange,
141
+ onChangeText,
142
+ onClick,
143
+ onFocus,
144
+ onKeyPress,
145
+ onSelectionChange,
146
+ onSubmitEditing,
147
+ placeholder,
148
+ placeholderTextColor = `rgba(0,0,0,0.2)`,
149
+ selectTextOnFocus,
150
+ spellCheck,
151
+ style = {},
152
+ value,
153
+ },
154
+ ref,
155
+ ) => {
156
+ const divRef = useRef<HTMLDivElement | null>(null);
157
+ const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
158
+ const contentSelection = useRef<Selection | null>(null);
159
+ const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
160
+ const history = useRef<InputHistory>();
161
+ if (!history.current) {
162
+ history.current = new InputHistory(100);
163
+ }
164
+
165
+ const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]);
166
+
167
+ // Empty placeholder would collapse the div, so we need to use zero-width space to prevent it
168
+ const heightSafePlaceholder = useMemo(() => getPlaceholderValue(placeholder), [placeholder]);
169
+
170
+ const parseText = useCallback(
171
+ (target: HTMLDivElement, text: string | null, customMarkdownStyles: MarkdownStyle, cursorPosition: number | null = null, shouldAddToHistory = true) => {
172
+ if (text === null) {
173
+ return {text: target.innerText, cursorPosition: null};
174
+ }
175
+ const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline);
176
+ if (history.current && shouldAddToHistory) {
177
+ history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition);
178
+ }
179
+
180
+ if (parsedText.cursorPosition !== null) {
181
+ contentSelection.current = {
182
+ start: parsedText.cursorPosition,
183
+ end: parsedText.cursorPosition,
184
+ };
185
+ }
186
+
187
+ return parsedText;
188
+ },
189
+ [multiline],
190
+ );
191
+
192
+ const processedMarkdownStyle = useMemo(() => {
193
+ const newMarkdownStyle = processMarkdownStyle(markdownStyle);
194
+ if (divRef.current) {
195
+ parseText(divRef.current, divRef.current.innerText, newMarkdownStyle);
196
+ }
197
+ return newMarkdownStyle;
198
+ }, [markdownStyle]);
199
+
200
+ const inputStyles = useMemo(
201
+ () =>
202
+ StyleSheet.flatten([
203
+ styles.defaultInputStyles,
204
+ flattenedStyle && {
205
+ caretColor: (flattenedStyle as TextStyle).color || 'black',
206
+ },
207
+ disabled && styles.disabledInputStyles,
208
+ createReactDOMStyle(preprocessStyle(flattenedStyle)),
209
+ ]) as CSSProperties,
210
+ [flattenedStyle, disabled],
211
+ );
212
+
213
+ const undo = useCallback(
214
+ (target: HTMLDivElement) => {
215
+ if (!history.current) return '';
216
+ const item = history.current.undo();
217
+ return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
218
+ },
219
+ [processedMarkdownStyle],
220
+ );
221
+
222
+ const redo = useCallback(
223
+ (target: HTMLDivElement) => {
224
+ if (!history.current) return '';
225
+ const item = history.current.redo();
226
+ return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
227
+ },
228
+ [processedMarkdownStyle],
229
+ );
230
+
231
+ // 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
232
+ const processedValue = useMemo(() => {
233
+ if (value && value[value.length - 1] === '\n') {
234
+ return `${value}\n`;
235
+ }
236
+ return value;
237
+ }, [value]);
238
+
239
+ const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
240
+ if (divRef.current) {
241
+ const text = normalizeValue(divRef.current.innerText || '');
242
+ if (typeof e.target !== 'number') {
243
+ // TODO: change the logic here so every event have value property
244
+ (e.target as HTMLInputElement).value = text;
245
+ }
246
+ e.nativeEvent.text = text;
247
+ }
248
+ return e;
249
+ }, []);
250
+
251
+ // Placeholder text color logic
252
+ const updateTextColor = useCallback((node: HTMLDivElement, text: string) => {
253
+ // eslint-disable-next-line no-param-reassign -- we need to change the style of the node, so we need to modify it
254
+ node.style.color = String(placeholder && (text === '' || text === '\n') ? placeholderTextColor : flattenedStyle.color || 'black');
255
+ }, []);
256
+
257
+ const handleOnChangeText = useCallback(
258
+ (e: SyntheticEvent<HTMLDivElement>) => {
259
+ if (!divRef.current || !(e.target instanceof HTMLElement)) {
260
+ return;
261
+ }
262
+
263
+ let text = '';
264
+ const nativeEvent = e.nativeEvent as MarkdownNativeEvent;
265
+ switch (nativeEvent.inputType) {
266
+ case 'historyUndo':
267
+ text = undo(divRef.current);
268
+ break;
269
+ case 'historyRedo':
270
+ text = redo(divRef.current);
271
+ break;
272
+ default:
273
+ text = parseText(divRef.current, e.target.innerText, processedMarkdownStyle).text;
274
+ }
275
+ updateTextColor(divRef.current, e.target.innerText);
276
+
277
+ if (onChange) {
278
+ const event = e as unknown as NativeSyntheticEvent<any>;
279
+ setEventProps(event);
280
+ onChange(event);
281
+ }
282
+
283
+ if (onChangeText) {
284
+ const normalizedText = normalizeValue(text);
285
+ onChangeText(normalizedText);
286
+ }
287
+ },
288
+ [multiline, onChange, onChangeText, setEventProps, processedMarkdownStyle],
289
+ );
290
+
291
+ const handleKeyPress = useCallback(
292
+ (e: KeyboardEvent<HTMLDivElement>) => {
293
+ if (!divRef.current) {
294
+ return;
295
+ }
296
+
297
+ const hostNode = e.target;
298
+ e.stopPropagation();
299
+
300
+ if (e.key === 'z' && e.metaKey) {
301
+ e.preventDefault();
302
+ const nativeEvent = e.nativeEvent as unknown as MarkdownNativeEvent;
303
+ if (e.shiftKey) {
304
+ nativeEvent.inputType = 'historyRedo';
305
+ } else {
306
+ nativeEvent.inputType = 'historyUndo';
307
+ }
308
+
309
+ handleOnChangeText(e);
310
+ return;
311
+ }
312
+
313
+ const blurOnSubmitDefault = !multiline;
314
+ const shouldBlurOnSubmit = blurOnSubmit === null ? blurOnSubmitDefault : blurOnSubmit;
315
+
316
+ const nativeEvent = e.nativeEvent;
317
+ const isComposing = isEventComposing(nativeEvent);
318
+
319
+ const event = e as unknown as NativeSyntheticEvent<TextInputKeyPressEventData>;
320
+ setEventProps(event);
321
+ if (onKeyPress) {
322
+ onKeyPress(event);
323
+ }
324
+
325
+ if (
326
+ e.key === 'Enter' &&
327
+ !e.shiftKey &&
328
+ // Do not call submit if composition is occuring.
329
+ !isComposing &&
330
+ !e.isDefaultPrevented()
331
+ ) {
332
+ // prevent "Enter" from inserting a newline or submitting a form
333
+ e.preventDefault();
334
+ if ((blurOnSubmit || !multiline) && onSubmitEditing) {
335
+ onSubmitEditing(event as unknown as NativeSyntheticEvent<TextInputSubmitEditingEventData>);
336
+ } else {
337
+ e.preventDefault();
338
+ // We need to change normal behavior of "Enter" key to insert a line breaks, to prevent wrapping contentEditable text in <div> tags.
339
+ // 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.
340
+ if (multiline) {
341
+ document.execCommand('insertLineBreak');
342
+ }
343
+ }
344
+
345
+ if ((shouldBlurOnSubmit && hostNode !== null) || !multiline) {
346
+ setTimeout(() => divRef.current && divRef.current.blur(), 0);
347
+ }
348
+ }
349
+ },
350
+ [onKeyPress],
351
+ );
352
+
353
+ const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
354
+ (event) => {
355
+ const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>;
356
+ setEventProps(e);
357
+ const selection = CursorUtils.getCurrentCursorPosition(e.target as unknown as HTMLElement);
358
+ contentSelection.current = selection;
359
+ if (onSelectionChange) {
360
+ e.nativeEvent.selection = selection;
361
+ onSelectionChange(e);
362
+ }
363
+ },
364
+ [onSelectionChange, setEventProps],
365
+ );
366
+
367
+ const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
368
+ (event) => {
369
+ const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
370
+ const hostNode = e.target as unknown as HTMLDivElement;
371
+ currentlyFocusedField.current = hostNode;
372
+ setEventProps(e);
373
+ if (divRef.current && contentSelection.current) {
374
+ CursorUtils.setCursorPosition(divRef.current, contentSelection.current.start || divRef.current.innerText.length, !multiline);
375
+ }
376
+
377
+ if (onFocus) {
378
+ setEventProps(e);
379
+ onFocus(e);
380
+ }
381
+
382
+ if (hostNode !== null) {
383
+ if (clearTextOnFocus && divRef.current) {
384
+ divRef.current.innerText = '';
385
+ }
386
+ if (selectTextOnFocus) {
387
+ // Safari requires selection to occur in a setTimeout
388
+ if (focusTimeout !== null) {
389
+ clearTimeout(focusTimeout);
390
+ }
391
+ focusTimeout = setTimeout(() => {
392
+ if (hostNode === null) {
393
+ return;
394
+ }
395
+ document.execCommand('selectAll', false, '');
396
+ }, 0);
397
+ }
398
+ }
399
+ },
400
+ [clearTextOnFocus, multiline, onFocus, selectTextOnFocus, setEventProps],
401
+ );
402
+
403
+ const handleBlur: FocusEventHandler<HTMLDivElement> = useCallback(
404
+ (event) => {
405
+ const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
406
+ currentlyFocusedField.current = null;
407
+ if (onBlur) {
408
+ setEventProps(e);
409
+ onBlur(e);
410
+ }
411
+ },
412
+ [onBlur, setEventProps],
413
+ );
414
+
415
+ const handleClick = useCallback(
416
+ (e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
417
+ if (!onClick || !divRef.current) {
418
+ return;
419
+ }
420
+ (e.target as HTMLInputElement).value = normalizeValue(divRef.current.innerText || '');
421
+ onClick(e);
422
+ },
423
+ [onClick],
424
+ );
425
+
426
+ const setRef = (currentRef: HTMLDivElement | null) => {
427
+ const r = currentRef;
428
+ if (r) {
429
+ (r as unknown as TextInput).isFocused = () => document.activeElement === r;
430
+ (r as unknown as TextInput).clear = () => {
431
+ r.innerText = '';
432
+ updateTextColor(r, '');
433
+ };
434
+
435
+ if (value === '' || value === undefined) {
436
+ // update to placeholder color when value is empty
437
+ updateTextColor(r, r.innerText);
438
+ }
439
+ }
440
+
441
+ if (ref) {
442
+ if (typeof ref === 'object') {
443
+ // eslint-disable-next-line no-param-reassign
444
+ (ref as MutableRefObject<HTMLDivElement | null>).current = r;
445
+ } else if (typeof ref === 'function') {
446
+ (ref as (elementRef: HTMLDivElement | null) => void)(r);
447
+ }
448
+ }
449
+ divRef.current = r;
450
+ };
451
+
452
+ useEffect(() => {
453
+ if (!divRef.current || processedValue === divRef.current.innerText) {
454
+ return;
455
+ }
456
+
457
+ if (value === undefined) {
458
+ parseText(divRef.current, divRef.current.innerText, processedMarkdownStyle);
459
+ return;
460
+ }
461
+
462
+ const text = processedValue !== undefined ? processedValue : '';
463
+ parseText(divRef.current, text, processedMarkdownStyle, text.length);
464
+ updateTextColor(divRef.current, value);
465
+ }, [multiline, processedMarkdownStyle, processedValue]);
466
+
467
+ useEffect(() => {
468
+ if (!divRef.current || !multiline) {
469
+ return;
470
+ }
471
+ const elementHeight = getElementHeight(divRef.current, inputStyles, numberOfLines);
472
+ divRef.current.style.height = elementHeight;
473
+ divRef.current.style.maxHeight = elementHeight;
474
+ }, [numberOfLines]);
475
+
476
+ return (
477
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
478
+ <div
479
+ ref={setRef}
480
+ contentEditable={!disabled}
481
+ style={inputStyles}
482
+ role={accessibilityRole || 'textbox'}
483
+ aria-label={accessibilityLabel}
484
+ aria-labelledby={`${accessibilityLabelledBy}`}
485
+ aria-placeholder={heightSafePlaceholder}
486
+ aria-multiline={multiline}
487
+ autoCorrect={autoCorrect ? 'on' : 'off'}
488
+ autoCapitalize={autoCapitalize}
489
+ className={className}
490
+ onKeyDown={handleKeyPress}
491
+ onInput={handleOnChangeText}
492
+ onSelect={handleSelectionChange}
493
+ onClick={handleClick}
494
+ onFocus={handleFocus}
495
+ onBlur={handleBlur}
496
+ placeholder={heightSafePlaceholder}
497
+ spellCheck={spellCheck}
498
+ dir={dir}
499
+ />
500
+ );
501
+ },
502
+ );
503
+
504
+ const styles = StyleSheet.create({
505
+ defaultInputStyles: {
506
+ borderColor: 'black',
507
+ borderWidth: 1,
508
+ borderStyle: 'solid',
509
+ fontFamily: 'sans-serif',
510
+ boxSizing: 'border-box',
511
+ whiteSpace: 'pre-wrap',
512
+ overflowY: 'scroll',
513
+ overflowX: 'scroll',
514
+ overflowWrap: 'break-word',
515
+ },
516
+ disabledInputStyles: {
517
+ opacity: 0.75,
518
+ cursor: 'default',
519
+ },
20
520
  });
21
521
 
22
522
  export default MarkdownTextInput;
@@ -0,0 +1 @@
1
+ declare module 'react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle';
@@ -0,0 +1,70 @@
1
+ import {Platform} from 'react-native';
2
+ import type * as MarkdownTextInputDecoractorView from './MarkdownTextInputDecoratorViewNativeComponent';
3
+
4
+ type MarkdownStyle = MarkdownTextInputDecoractorView.MarkdownStyle;
5
+
6
+ type PartialMarkdownStyle = Partial<{
7
+ [K in keyof MarkdownStyle]: Partial<MarkdownStyle[K]>;
8
+ }>;
9
+
10
+ const FONT_FAMILY_MONOSPACE = Platform.select({
11
+ ios: 'Courier',
12
+ default: 'monospace',
13
+ });
14
+
15
+ function makeDefaultMarkdownStyle(): MarkdownStyle {
16
+ return {
17
+ syntax: {
18
+ color: 'gray',
19
+ },
20
+ link: {
21
+ color: 'blue',
22
+ },
23
+ h1: {
24
+ fontSize: 25,
25
+ },
26
+ blockquote: {
27
+ borderColor: 'gray',
28
+ borderWidth: 6,
29
+ marginLeft: 6,
30
+ paddingLeft: 6,
31
+ },
32
+ code: {
33
+ fontFamily: FONT_FAMILY_MONOSPACE,
34
+ color: 'black',
35
+ backgroundColor: 'lightgray',
36
+ },
37
+ pre: {
38
+ fontFamily: FONT_FAMILY_MONOSPACE,
39
+ color: 'black',
40
+ backgroundColor: 'lightgray',
41
+ },
42
+ mentionHere: {
43
+ color: 'green',
44
+ backgroundColor: 'lime',
45
+ },
46
+ mentionUser: {
47
+ color: 'blue',
48
+ backgroundColor: 'cyan',
49
+ },
50
+ };
51
+ }
52
+
53
+ function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined): MarkdownStyle {
54
+ const output = makeDefaultMarkdownStyle();
55
+
56
+ if (input !== undefined) {
57
+ Object.keys(input).forEach((key) => {
58
+ if (!(key in output)) {
59
+ return;
60
+ }
61
+ Object.assign(output[key as keyof MarkdownStyle], input[key as keyof MarkdownStyle]);
62
+ });
63
+ }
64
+
65
+ return output;
66
+ }
67
+
68
+ export type {PartialMarkdownStyle};
69
+
70
+ export {mergeMarkdownStyleWithDefault};