@expensify/react-native-live-markdown 0.1.16 → 0.1.18
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.web.js +76 -38
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/web/cursorUtils.js +51 -51
- package/lib/commonjs/web/cursorUtils.js.map +1 -1
- package/lib/commonjs/web/parserUtils.js +9 -6
- package/lib/commonjs/web/parserUtils.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +76 -38
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/web/cursorUtils.js +51 -52
- package/lib/module/web/cursorUtils.js.map +1 -1
- package/lib/module/web/parserUtils.js +9 -6
- package/lib/module/web/parserUtils.js.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/web/cursorUtils.d.ts +4 -3
- package/lib/typescript/src/web/cursorUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/parserUtils.d.ts +1 -1
- package/lib/typescript/src/web/parserUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/MarkdownTextInput.web.tsx +82 -44
- package/src/web/cursorUtils.ts +55 -54
- package/src/web/parserUtils.ts +9 -13
|
@@ -150,15 +150,18 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
150
150
|
placeholderTextColor = `rgba(0,0,0,0.2)`,
|
|
151
151
|
selectTextOnFocus,
|
|
152
152
|
spellCheck,
|
|
153
|
+
selection,
|
|
153
154
|
style = {},
|
|
154
155
|
value,
|
|
155
156
|
autoFocus = false,
|
|
156
157
|
},
|
|
157
158
|
ref,
|
|
158
159
|
) => {
|
|
160
|
+
const compositionRef = useRef<boolean>(false);
|
|
159
161
|
const divRef = useRef<HTMLDivElement | null>(null);
|
|
160
162
|
const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
|
|
161
|
-
const
|
|
163
|
+
const valueLength = value ? value.length : 0;
|
|
164
|
+
const contentSelection = useRef<Selection>({start: valueLength, end: valueLength});
|
|
162
165
|
const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
|
|
163
166
|
const history = useRef<InputHistory>();
|
|
164
167
|
if (!history.current) {
|
|
@@ -170,15 +173,18 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
170
173
|
// Empty placeholder would collapse the div, so we need to use zero-width space to prevent it
|
|
171
174
|
const heightSafePlaceholder = useMemo(() => getPlaceholderValue(placeholder), [placeholder]);
|
|
172
175
|
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
+
const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
|
|
177
|
+
if (divRef.current) {
|
|
178
|
+
const text = normalizeValue(divRef.current.innerText || '');
|
|
179
|
+
if (e.target) {
|
|
180
|
+
// TODO: change the logic here so every event have value property
|
|
181
|
+
(e.target as unknown as HTMLInputElement).value = text;
|
|
182
|
+
}
|
|
183
|
+
if (e.nativeEvent && e.nativeEvent.text) {
|
|
184
|
+
e.nativeEvent.text = text;
|
|
185
|
+
}
|
|
176
186
|
}
|
|
177
|
-
|
|
178
|
-
const markdownHTMLInput = divRef.current as HTMLInputElement;
|
|
179
|
-
markdownHTMLInput.selectionStart = selection.start;
|
|
180
|
-
markdownHTMLInput.selectionEnd = selection.end;
|
|
181
|
-
contentSelection.current = selection;
|
|
187
|
+
return e;
|
|
182
188
|
}, []);
|
|
183
189
|
|
|
184
190
|
const parseText = useCallback(
|
|
@@ -191,8 +197,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
191
197
|
history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition);
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
updateSelection();
|
|
195
|
-
|
|
196
200
|
return parsedText;
|
|
197
201
|
},
|
|
198
202
|
[multiline],
|
|
@@ -245,18 +249,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
245
249
|
return value;
|
|
246
250
|
}, [value]);
|
|
247
251
|
|
|
248
|
-
const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
|
|
249
|
-
if (divRef.current) {
|
|
250
|
-
const text = normalizeValue(divRef.current.innerText || '');
|
|
251
|
-
if (typeof e.target !== 'number') {
|
|
252
|
-
// TODO: change the logic here so every event have value property
|
|
253
|
-
(e.target as unknown as HTMLInputElement).value = text;
|
|
254
|
-
}
|
|
255
|
-
e.nativeEvent.text = text;
|
|
256
|
-
}
|
|
257
|
-
return e;
|
|
258
|
-
}, []);
|
|
259
|
-
|
|
260
252
|
// Placeholder text color logic
|
|
261
253
|
const updateTextColor = useCallback((node: HTMLDivElement, text: string) => {
|
|
262
254
|
// eslint-disable-next-line no-param-reassign -- we need to change the style of the node, so we need to modify it
|
|
@@ -269,6 +261,12 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
269
261
|
return;
|
|
270
262
|
}
|
|
271
263
|
|
|
264
|
+
if (compositionRef.current) {
|
|
265
|
+
updateTextColor(divRef.current, e.target.innerText);
|
|
266
|
+
compositionRef.current = false;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
272
270
|
let text = '';
|
|
273
271
|
const nativeEvent = e.nativeEvent as MarkdownNativeEvent;
|
|
274
272
|
switch (nativeEvent.inputType) {
|
|
@@ -297,6 +295,41 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
297
295
|
[multiline, onChange, onChangeText, setEventProps, processedMarkdownStyle],
|
|
298
296
|
);
|
|
299
297
|
|
|
298
|
+
const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
|
|
299
|
+
(event) => {
|
|
300
|
+
const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>;
|
|
301
|
+
setEventProps(e);
|
|
302
|
+
if (onSelectionChange && contentSelection.current) {
|
|
303
|
+
e.nativeEvent.selection = contentSelection.current;
|
|
304
|
+
onSelectionChange(e);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[onSelectionChange, setEventProps],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const updateRefSelectionVariables = useCallback((newSelection: Selection) => {
|
|
311
|
+
const {start, end} = newSelection;
|
|
312
|
+
const markdownHTMLInput = divRef.current as HTMLInputElement;
|
|
313
|
+
markdownHTMLInput.selectionStart = start;
|
|
314
|
+
markdownHTMLInput.selectionEnd = end;
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
const updateSelection = useCallback((e: SyntheticEvent<HTMLDivElement> | null = null) => {
|
|
318
|
+
if (!divRef.current) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const newSelection = CursorUtils.getCurrentCursorPosition(divRef.current);
|
|
322
|
+
|
|
323
|
+
if (newSelection && (contentSelection.current.start !== newSelection.start || contentSelection.current.end !== newSelection.end)) {
|
|
324
|
+
updateRefSelectionVariables(newSelection);
|
|
325
|
+
contentSelection.current = newSelection;
|
|
326
|
+
|
|
327
|
+
if (e) {
|
|
328
|
+
handleSelectionChange(e);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
300
333
|
const handleKeyPress = useCallback(
|
|
301
334
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
302
335
|
if (!divRef.current) {
|
|
@@ -331,6 +364,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
331
364
|
onKeyPress(event);
|
|
332
365
|
}
|
|
333
366
|
|
|
367
|
+
updateSelection(event as unknown as SyntheticEvent<HTMLDivElement, Event>);
|
|
368
|
+
|
|
334
369
|
if (
|
|
335
370
|
e.key === 'Enter' &&
|
|
336
371
|
!e.shiftKey &&
|
|
@@ -359,19 +394,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
359
394
|
[onKeyPress],
|
|
360
395
|
);
|
|
361
396
|
|
|
362
|
-
const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
|
|
363
|
-
(event) => {
|
|
364
|
-
const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>;
|
|
365
|
-
setEventProps(e);
|
|
366
|
-
updateSelection();
|
|
367
|
-
if (onSelectionChange && contentSelection.current) {
|
|
368
|
-
e.nativeEvent.selection = contentSelection.current;
|
|
369
|
-
onSelectionChange(e);
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
[onSelectionChange, setEventProps],
|
|
373
|
-
);
|
|
374
|
-
|
|
375
397
|
const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
|
|
376
398
|
(event) => {
|
|
377
399
|
const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
|
|
@@ -379,7 +401,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
379
401
|
currentlyFocusedField.current = hostNode;
|
|
380
402
|
setEventProps(e);
|
|
381
403
|
if (divRef.current && contentSelection.current) {
|
|
382
|
-
CursorUtils.setCursorPosition(divRef.current, contentSelection.current.
|
|
404
|
+
CursorUtils.setCursorPosition(divRef.current, contentSelection.current.end || contentSelection.current.start);
|
|
383
405
|
}
|
|
384
406
|
|
|
385
407
|
if (onFocus) {
|
|
@@ -411,6 +433,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
411
433
|
const handleBlur: FocusEventHandler<HTMLDivElement> = useCallback(
|
|
412
434
|
(event) => {
|
|
413
435
|
const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
|
|
436
|
+
CursorUtils.removeSelection();
|
|
414
437
|
currentlyFocusedField.current = null;
|
|
415
438
|
if (onBlur) {
|
|
416
439
|
setEventProps(e);
|
|
@@ -422,7 +445,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
422
445
|
|
|
423
446
|
const handleClick = useCallback(
|
|
424
447
|
(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
|
|
425
|
-
updateSelection();
|
|
448
|
+
updateSelection(e);
|
|
426
449
|
if (!onClick || !divRef.current) {
|
|
427
450
|
return;
|
|
428
451
|
}
|
|
@@ -432,6 +455,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
432
455
|
[onClick],
|
|
433
456
|
);
|
|
434
457
|
|
|
458
|
+
const startComposition = useCallback(() => {
|
|
459
|
+
compositionRef.current = true;
|
|
460
|
+
}, []);
|
|
461
|
+
|
|
435
462
|
const setRef = (currentRef: HTMLDivElement | null) => {
|
|
436
463
|
const r = currentRef;
|
|
437
464
|
if (r) {
|
|
@@ -507,13 +534,24 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
507
534
|
}, []);
|
|
508
535
|
|
|
509
536
|
useEffect(() => {
|
|
510
|
-
|
|
511
|
-
if (!(divRef.current && autoFocus)) {
|
|
537
|
+
if (!divRef.current) {
|
|
512
538
|
return;
|
|
513
539
|
}
|
|
514
|
-
|
|
540
|
+
// focus the input on mount if autoFocus is set
|
|
541
|
+
if (autoFocus) {
|
|
542
|
+
divRef.current.focus();
|
|
543
|
+
}
|
|
544
|
+
updateRefSelectionVariables(contentSelection.current);
|
|
515
545
|
}, []);
|
|
516
546
|
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
if (!divRef.current || !selection || (selection.start === contentSelection.current.start && selection.end === contentSelection.current.end)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
CursorUtils.setCursorPosition(divRef.current, selection.start, selection.end);
|
|
552
|
+
updateSelection();
|
|
553
|
+
}, [selection]);
|
|
554
|
+
|
|
517
555
|
return (
|
|
518
556
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
519
557
|
<div
|
|
@@ -529,9 +567,9 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
529
567
|
autoCapitalize={autoCapitalize}
|
|
530
568
|
className={className}
|
|
531
569
|
onKeyDown={handleKeyPress}
|
|
570
|
+
onCompositionStart={startComposition}
|
|
532
571
|
onKeyUp={updateSelection}
|
|
533
572
|
onInput={handleOnChangeText}
|
|
534
|
-
onSelect={handleSelectionChange}
|
|
535
573
|
onClick={handleClick}
|
|
536
574
|
onFocus={handleFocus}
|
|
537
575
|
onBlur={handleBlur}
|
|
@@ -552,8 +590,8 @@ const styles = StyleSheet.create({
|
|
|
552
590
|
// @ts-expect-error it works on web
|
|
553
591
|
boxSizing: 'border-box',
|
|
554
592
|
whiteSpace: 'pre-wrap',
|
|
555
|
-
overflowY: '
|
|
556
|
-
overflowX: '
|
|
593
|
+
overflowY: 'auto',
|
|
594
|
+
overflowX: 'auto',
|
|
557
595
|
overflowWrap: 'break-word',
|
|
558
596
|
},
|
|
559
597
|
disabledInputStyles: {
|
package/src/web/cursorUtils.ts
CHANGED
|
@@ -1,46 +1,53 @@
|
|
|
1
|
-
function
|
|
1
|
+
function findTextNodes(textNodes: Text[], node: ChildNode) {
|
|
2
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
3
|
+
textNodes.push(node as Text);
|
|
4
|
+
} else {
|
|
5
|
+
for (let i = 0, length = node.childNodes.length; i < length; ++i) {
|
|
6
|
+
const childNode = node.childNodes[i];
|
|
7
|
+
if (childNode) {
|
|
8
|
+
findTextNodes(textNodes, childNode);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function setCursorPosition(target: HTMLElement, start: number, end: number | null = null) {
|
|
2
15
|
const range = document.createRange();
|
|
3
|
-
range.
|
|
16
|
+
range.selectNodeContents(target);
|
|
4
17
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const textNodes: Text[] = [];
|
|
19
|
+
findTextNodes(textNodes, target);
|
|
20
|
+
|
|
21
|
+
let charCount = 0;
|
|
22
|
+
let startNode: Text | null = null;
|
|
23
|
+
let endNode: Text | null = null;
|
|
24
|
+
const n = textNodes.length;
|
|
25
|
+
for (let i = 0; i < n; ++i) {
|
|
26
|
+
const textNode = textNodes[i];
|
|
27
|
+
if (textNode) {
|
|
28
|
+
const nextCharCount = charCount + textNode.length;
|
|
29
|
+
|
|
30
|
+
if (!startNode && start >= charCount && (start <= nextCharCount || (start === nextCharCount && i < n - 1))) {
|
|
31
|
+
startNode = textNode;
|
|
32
|
+
range.setStart(textNode, start - charCount);
|
|
33
|
+
if (!end) {
|
|
34
|
+
break;
|
|
21
35
|
}
|
|
22
|
-
return range;
|
|
23
36
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const currentNode = current.childNodes[i];
|
|
28
|
-
if (currentNode && (!ignoreNewLines || (ignoreNewLines && currentNode.nodeName !== 'BR'))) {
|
|
29
|
-
stack.push(currentNode);
|
|
30
|
-
}
|
|
37
|
+
if (end && !endNode && end >= charCount && (end <= nextCharCount || (end === nextCharCount && i < n - 1))) {
|
|
38
|
+
endNode = textNode;
|
|
39
|
+
range.setEnd(textNode, end - charCount);
|
|
31
40
|
}
|
|
41
|
+
charCount = nextCharCount;
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
45
|
+
if (!end) {
|
|
46
|
+
range.collapse(true);
|
|
47
|
+
}
|
|
38
48
|
|
|
39
|
-
function setCursorPosition(target: HTMLElement, targetPosition: number, ignoreNewLines = false) {
|
|
40
|
-
const range = createRange(target, targetPosition, ignoreNewLines);
|
|
41
49
|
const selection = window.getSelection();
|
|
42
50
|
if (selection) {
|
|
43
|
-
range.collapse(true);
|
|
44
51
|
selection.removeAllRanges();
|
|
45
52
|
selection.addRange(range);
|
|
46
53
|
}
|
|
@@ -57,31 +64,25 @@ function moveCursorToEnd(target: HTMLElement) {
|
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function getIndexedPosition(target: HTMLElement, range: Range, isStart: boolean) {
|
|
61
|
-
const marker = document.createTextNode('\0');
|
|
62
|
-
const rangeClone = range.cloneRange();
|
|
63
|
-
|
|
64
|
-
rangeClone.collapse(isStart);
|
|
65
|
-
|
|
66
|
-
rangeClone.insertNode(marker);
|
|
67
|
-
const position = target.innerText.indexOf('\0');
|
|
68
|
-
if (marker.parentNode) {
|
|
69
|
-
marker.parentNode.removeChild(marker);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return position;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
67
|
function getCurrentCursorPosition(target: HTMLElement) {
|
|
76
|
-
const selection =
|
|
77
|
-
if (!selection || selection.rangeCount === 0) {
|
|
78
|
-
return
|
|
68
|
+
const selection = window.getSelection();
|
|
69
|
+
if (!selection || (selection && selection.rangeCount === 0)) {
|
|
70
|
+
return null;
|
|
79
71
|
}
|
|
80
|
-
|
|
81
72
|
const range = selection.getRangeAt(0);
|
|
82
|
-
const
|
|
83
|
-
|
|
73
|
+
const preSelectionRange = range.cloneRange();
|
|
74
|
+
preSelectionRange.selectNodeContents(target);
|
|
75
|
+
preSelectionRange.setEnd(range.startContainer, range.startOffset);
|
|
76
|
+
const start = preSelectionRange.toString().length;
|
|
77
|
+
const end = start + range.toString().length;
|
|
84
78
|
return {start, end};
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
function removeSelection() {
|
|
82
|
+
const selection = window.getSelection();
|
|
83
|
+
if (selection) {
|
|
84
|
+
selection.removeAllRanges();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, removeSelection};
|
package/src/web/parserUtils.ts
CHANGED
|
@@ -160,20 +160,14 @@ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownS
|
|
|
160
160
|
return root;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function parseText(
|
|
164
|
-
target: HTMLElement,
|
|
165
|
-
text: string,
|
|
166
|
-
curosrPositionIndex: number | null,
|
|
167
|
-
markdownStyle: PartialMarkdownStyle = {},
|
|
168
|
-
disableNewLinesInCursorPositioning = false,
|
|
169
|
-
alwaysMoveCursorToTheEnd = false,
|
|
170
|
-
) {
|
|
163
|
+
function parseText(target: HTMLElement, text: string, curosrPositionIndex: number | null, markdownStyle: PartialMarkdownStyle = {}, alwaysMoveCursorToTheEnd = false) {
|
|
171
164
|
const targetElement = target;
|
|
172
165
|
|
|
173
166
|
let cursorPosition: number | null = curosrPositionIndex;
|
|
174
167
|
const isFocused = document.activeElement === target;
|
|
175
168
|
if (isFocused && curosrPositionIndex === null) {
|
|
176
|
-
|
|
169
|
+
const selection = CursorUtils.getCurrentCursorPosition(target);
|
|
170
|
+
cursorPosition = selection ? selection.end : null;
|
|
177
171
|
}
|
|
178
172
|
const ranges = global.parseExpensiMarkToRanges(text);
|
|
179
173
|
|
|
@@ -188,10 +182,12 @@ function parseText(
|
|
|
188
182
|
target.appendChild(dom);
|
|
189
183
|
}
|
|
190
184
|
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
if (isFocused) {
|
|
186
|
+
if (alwaysMoveCursorToTheEnd || cursorPosition === null) {
|
|
187
|
+
CursorUtils.moveCursorToEnd(target);
|
|
188
|
+
} else if (cursorPosition !== null) {
|
|
189
|
+
CursorUtils.setCursorPosition(target, cursorPosition);
|
|
190
|
+
}
|
|
195
191
|
}
|
|
196
192
|
|
|
197
193
|
return {text: target.innerText, cursorPosition: cursorPosition || 0};
|