@expensify/react-native-live-markdown 0.1.3 → 0.1.5
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.
- package/lib/commonjs/MarkdownTextInput.js +5 -55
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +399 -9
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/react-native-web.d.js +2 -0
- package/lib/commonjs/react-native-web.d.js.map +1 -0
- package/lib/commonjs/styleUtils.js +61 -0
- package/lib/commonjs/styleUtils.js.map +1 -0
- package/lib/commonjs/web/InputHistory.js +92 -0
- package/lib/commonjs/web/InputHistory.js.map +1 -0
- package/lib/commonjs/web/MarkdownTextInput.css +24 -0
- package/lib/commonjs/web/cursorUtils.js +90 -0
- package/lib/commonjs/web/cursorUtils.js.map +1 -0
- package/lib/commonjs/web/parserUtils.js +170 -0
- package/lib/commonjs/web/parserUtils.js.map +1 -0
- package/lib/module/MarkdownTextInput.js +3 -55
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +399 -12
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/react-native-web.d.js +2 -0
- package/lib/module/react-native-web.d.js.map +1 -0
- package/lib/module/styleUtils.js +56 -0
- package/lib/module/styleUtils.js.map +1 -0
- package/lib/module/web/InputHistory.js +85 -0
- package/lib/module/web/InputHistory.js.map +1 -0
- package/lib/module/web/MarkdownTextInput.css +24 -0
- package/lib/module/web/cursorUtils.js +83 -0
- package/lib/module/web/cursorUtils.js.map +1 -0
- package/lib/module/web/parserUtils.js +162 -0
- package/lib/module/web/parserUtils.js.map +1 -0
- package/lib/parser/react-native-live-markdown-parser.js +34 -0
- package/lib/typescript/src/MarkdownTextInput.d.ts +3 -6
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -2
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/styleUtils.d.ts +9 -0
- package/lib/typescript/src/styleUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/InputHistory.d.ts +24 -0
- package/lib/typescript/src/web/InputHistory.d.ts.map +1 -0
- package/lib/typescript/src/web/cursorUtils.d.ts +8 -0
- package/lib/typescript/src/web/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/parserUtils.d.ts +16 -0
- package/lib/typescript/src/web/parserUtils.d.ts.map +1 -0
- package/package.json +6 -3
- package/parser/react-native-live-markdown-parser.js +7 -7
- package/src/MarkdownTextInput.tsx +9 -69
- package/src/MarkdownTextInput.web.tsx +516 -16
- package/src/react-native-web.d.ts +1 -0
- package/src/styleUtils.ts +70 -0
- package/src/web/InputHistory.ts +114 -0
- package/src/web/MarkdownTextInput.css +24 -0
- package/src/web/cursorUtils.ts +87 -0
- 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
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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};
|