@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.
- package/lib/commonjs/MarkdownTextInput.js +2 -4
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +105 -149
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/commonjs/web/utils/blockUtils.js +77 -0
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
- package/lib/commonjs/web/utils/browserUtils.js +18 -0
- package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/cursorUtils.js +111 -0
- package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
- package/lib/commonjs/web/utils/inputUtils.js +108 -0
- package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +266 -0
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/treeUtils.js +85 -0
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
- package/lib/commonjs/web/utils/webStyleUtils.js +51 -0
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
- package/lib/module/MarkdownTextInput.js +2 -2
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +101 -145
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/module/web/utils/blockUtils.js +72 -0
- package/lib/module/web/utils/blockUtils.js.map +1 -0
- package/lib/module/web/utils/browserUtils.js +12 -0
- package/lib/module/web/utils/browserUtils.js.map +1 -0
- package/lib/module/web/utils/cursorUtils.js +102 -0
- package/lib/module/web/utils/cursorUtils.js.map +1 -0
- package/lib/module/web/utils/inputUtils.js +99 -0
- package/lib/module/web/utils/inputUtils.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +260 -0
- package/lib/module/web/utils/parserUtils.js.map +1 -0
- package/lib/module/web/utils/treeUtils.js +78 -0
- package/lib/module/web/utils/treeUtils.js.map +1 -0
- package/lib/module/web/utils/webStyleUtils.js +45 -0
- package/lib/module/web/utils/webStyleUtils.js.map +1 -0
- package/lib/typescript/src/MarkdownTextInput.d.ts +1 -2
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -3
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/styleUtils.d.ts +1 -2
- package/lib/typescript/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/utils/blockUtils.d.ts +5 -0
- package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/browserUtils.d.ts +12 -0
- package/lib/typescript/src/web/utils/browserUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/cursorUtils.d.ts +12 -0
- package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/inputUtils.d.ts +9 -0
- package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/parserUtils.d.ts +22 -0
- package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/treeUtils.d.ts +17 -0
- package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/webStyleUtils.d.ts +6 -0
- package/lib/typescript/src/web/utils/webStyleUtils.d.ts.map +1 -0
- package/package.json +5 -2
- package/src/MarkdownTextInput.tsx +4 -7
- package/src/MarkdownTextInput.web.tsx +142 -171
- package/src/styleUtils.ts +1 -3
- package/src/web/utils/blockUtils.ts +72 -0
- package/src/web/utils/browserUtils.ts +13 -0
- package/src/web/utils/cursorUtils.ts +116 -0
- package/src/web/utils/inputUtils.ts +106 -0
- package/src/web/utils/parserUtils.ts +302 -0
- package/src/web/utils/treeUtils.ts +103 -0
- package/src/web/utils/webStyleUtils.ts +54 -0
- package/lib/commonjs/web/browserUtils.js +0 -16
- package/lib/commonjs/web/browserUtils.js.map +0 -1
- package/lib/commonjs/web/cursorUtils.js +0 -167
- package/lib/commonjs/web/cursorUtils.js.map +0 -1
- package/lib/commonjs/web/parserUtils.js +0 -214
- package/lib/commonjs/web/parserUtils.js.map +0 -1
- package/lib/module/web/browserUtils.js +0 -11
- package/lib/module/web/browserUtils.js.map +0 -1
- package/lib/module/web/cursorUtils.js +0 -154
- package/lib/module/web/cursorUtils.js.map +0 -1
- package/lib/module/web/parserUtils.js +0 -206
- package/lib/module/web/parserUtils.js.map +0 -1
- package/lib/typescript/src/web/browserUtils.d.ts +0 -10
- package/lib/typescript/src/web/browserUtils.d.ts.map +0 -1
- package/lib/typescript/src/web/cursorUtils.d.ts +0 -12
- package/lib/typescript/src/web/cursorUtils.d.ts.map +0 -1
- package/lib/typescript/src/web/parserUtils.d.ts +0 -17
- package/lib/typescript/src/web/parserUtils.d.ts.map +0 -1
- package/src/web/browserUtils.ts +0 -11
- package/src/web/cursorUtils.ts +0 -177
- 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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
(
|
|
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:
|
|
149
|
+
return {text: divRef.current.value, cursorPosition: null};
|
|
214
150
|
}
|
|
215
|
-
const parsedText =
|
|
151
|
+
const parsedText = updateInputStructure(target, text, cursorPosition, customMarkdownStyles, !multiline, shouldForceDOMUpdate);
|
|
152
|
+
divRef.current.value = parsedText.text;
|
|
153
|
+
|
|
216
154
|
if (history.current && shouldAddToHistory) {
|
|
217
|
-
|
|
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.
|
|
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
|
-
|
|
179
|
+
parseToReactDOMStyle(flattenedStyle),
|
|
243
180
|
]) as CSSProperties,
|
|
244
181
|
[flattenedStyle, disabled],
|
|
245
182
|
);
|
|
246
183
|
|
|
247
184
|
const undo = useCallback(
|
|
248
|
-
(target:
|
|
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 ?
|
|
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:
|
|
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 ?
|
|
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 ||
|
|
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
|
|
357
|
-
|
|
296
|
+
const newCursorPosition = Math.max(Math.max(contentSelection.current.end, 0) + (parsedText.length - previousText.length), 0);
|
|
297
|
+
|
|
358
298
|
if (compositionRef.current) {
|
|
359
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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(
|
|
361
|
+
onChangeText(text);
|
|
433
362
|
}
|
|
434
363
|
|
|
435
364
|
handleContentSizeChange();
|
|
436
365
|
},
|
|
437
|
-
[updateTextColor,
|
|
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
|
-
|
|
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
|
-
|
|
457
|
+
setCursorPosition(divRef.current, contentSelection.current.start, contentSelection.current.end);
|
|
510
458
|
} else {
|
|
511
|
-
const valueLength = value ? value.length : divRef.current.
|
|
512
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
569
|
-
|
|
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
|
|
576
|
-
|
|
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.
|
|
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.
|
|
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 ||
|
|
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.
|
|
596
|
+
parseText(divRef.current, divRef.current.value, processedMarkdownStyle);
|
|
626
597
|
return;
|
|
627
598
|
}
|
|
628
|
-
|
|
629
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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;
|