@expensify/react-native-live-markdown 0.1.17 → 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 +69 -41
- 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 +69 -41
- 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 +73 -47
- package/src/web/cursorUtils.ts +55 -54
- package/src/web/parserUtils.ts +9 -13
|
@@ -150,6 +150,7 @@ 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,
|
|
@@ -159,7 +160,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
159
160
|
const compositionRef = useRef<boolean>(false);
|
|
160
161
|
const divRef = useRef<HTMLDivElement | null>(null);
|
|
161
162
|
const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
|
|
162
|
-
const
|
|
163
|
+
const valueLength = value ? value.length : 0;
|
|
164
|
+
const contentSelection = useRef<Selection>({start: valueLength, end: valueLength});
|
|
163
165
|
const className = `react-native-live-markdown-input-${multiline ? 'multiline' : 'singleline'}`;
|
|
164
166
|
const history = useRef<InputHistory>();
|
|
165
167
|
if (!history.current) {
|
|
@@ -171,15 +173,18 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
171
173
|
// Empty placeholder would collapse the div, so we need to use zero-width space to prevent it
|
|
172
174
|
const heightSafePlaceholder = useMemo(() => getPlaceholderValue(placeholder), [placeholder]);
|
|
173
175
|
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
|
|
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
|
+
}
|
|
177
186
|
}
|
|
178
|
-
|
|
179
|
-
const markdownHTMLInput = divRef.current as HTMLInputElement;
|
|
180
|
-
markdownHTMLInput.selectionStart = selection.start;
|
|
181
|
-
markdownHTMLInput.selectionEnd = selection.end;
|
|
182
|
-
contentSelection.current = selection;
|
|
187
|
+
return e;
|
|
183
188
|
}, []);
|
|
184
189
|
|
|
185
190
|
const parseText = useCallback(
|
|
@@ -192,8 +197,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
192
197
|
history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition);
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
updateSelection();
|
|
196
|
-
|
|
197
200
|
return parsedText;
|
|
198
201
|
},
|
|
199
202
|
[multiline],
|
|
@@ -246,18 +249,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
246
249
|
return value;
|
|
247
250
|
}, [value]);
|
|
248
251
|
|
|
249
|
-
const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
|
|
250
|
-
if (divRef.current) {
|
|
251
|
-
const text = normalizeValue(divRef.current.innerText || '');
|
|
252
|
-
if (typeof e.target !== 'number') {
|
|
253
|
-
// TODO: change the logic here so every event have value property
|
|
254
|
-
(e.target as unknown as HTMLInputElement).value = text;
|
|
255
|
-
}
|
|
256
|
-
e.nativeEvent.text = text;
|
|
257
|
-
}
|
|
258
|
-
return e;
|
|
259
|
-
}, []);
|
|
260
|
-
|
|
261
252
|
// Placeholder text color logic
|
|
262
253
|
const updateTextColor = useCallback((node: HTMLDivElement, text: string) => {
|
|
263
254
|
// eslint-disable-next-line no-param-reassign -- we need to change the style of the node, so we need to modify it
|
|
@@ -304,6 +295,41 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
304
295
|
[multiline, onChange, onChangeText, setEventProps, processedMarkdownStyle],
|
|
305
296
|
);
|
|
306
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
|
+
|
|
307
333
|
const handleKeyPress = useCallback(
|
|
308
334
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
309
335
|
if (!divRef.current) {
|
|
@@ -338,6 +364,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
338
364
|
onKeyPress(event);
|
|
339
365
|
}
|
|
340
366
|
|
|
367
|
+
updateSelection(event as unknown as SyntheticEvent<HTMLDivElement, Event>);
|
|
368
|
+
|
|
341
369
|
if (
|
|
342
370
|
e.key === 'Enter' &&
|
|
343
371
|
!e.shiftKey &&
|
|
@@ -366,19 +394,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
366
394
|
[onKeyPress],
|
|
367
395
|
);
|
|
368
396
|
|
|
369
|
-
const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
|
|
370
|
-
(event) => {
|
|
371
|
-
const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>;
|
|
372
|
-
setEventProps(e);
|
|
373
|
-
updateSelection();
|
|
374
|
-
if (onSelectionChange && contentSelection.current) {
|
|
375
|
-
e.nativeEvent.selection = contentSelection.current;
|
|
376
|
-
onSelectionChange(e);
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
[onSelectionChange, setEventProps],
|
|
380
|
-
);
|
|
381
|
-
|
|
382
397
|
const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
|
|
383
398
|
(event) => {
|
|
384
399
|
const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
|
|
@@ -386,7 +401,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
386
401
|
currentlyFocusedField.current = hostNode;
|
|
387
402
|
setEventProps(e);
|
|
388
403
|
if (divRef.current && contentSelection.current) {
|
|
389
|
-
CursorUtils.setCursorPosition(divRef.current, contentSelection.current.
|
|
404
|
+
CursorUtils.setCursorPosition(divRef.current, contentSelection.current.end || contentSelection.current.start);
|
|
390
405
|
}
|
|
391
406
|
|
|
392
407
|
if (onFocus) {
|
|
@@ -418,6 +433,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
418
433
|
const handleBlur: FocusEventHandler<HTMLDivElement> = useCallback(
|
|
419
434
|
(event) => {
|
|
420
435
|
const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>;
|
|
436
|
+
CursorUtils.removeSelection();
|
|
421
437
|
currentlyFocusedField.current = null;
|
|
422
438
|
if (onBlur) {
|
|
423
439
|
setEventProps(e);
|
|
@@ -429,7 +445,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
429
445
|
|
|
430
446
|
const handleClick = useCallback(
|
|
431
447
|
(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
|
|
432
|
-
updateSelection();
|
|
448
|
+
updateSelection(e);
|
|
433
449
|
if (!onClick || !divRef.current) {
|
|
434
450
|
return;
|
|
435
451
|
}
|
|
@@ -439,6 +455,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
439
455
|
[onClick],
|
|
440
456
|
);
|
|
441
457
|
|
|
458
|
+
const startComposition = useCallback(() => {
|
|
459
|
+
compositionRef.current = true;
|
|
460
|
+
}, []);
|
|
461
|
+
|
|
442
462
|
const setRef = (currentRef: HTMLDivElement | null) => {
|
|
443
463
|
const r = currentRef;
|
|
444
464
|
if (r) {
|
|
@@ -514,16 +534,23 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
514
534
|
}, []);
|
|
515
535
|
|
|
516
536
|
useEffect(() => {
|
|
517
|
-
|
|
518
|
-
if (!(divRef.current && autoFocus)) {
|
|
537
|
+
if (!divRef.current) {
|
|
519
538
|
return;
|
|
520
539
|
}
|
|
521
|
-
|
|
540
|
+
// focus the input on mount if autoFocus is set
|
|
541
|
+
if (autoFocus) {
|
|
542
|
+
divRef.current.focus();
|
|
543
|
+
}
|
|
544
|
+
updateRefSelectionVariables(contentSelection.current);
|
|
522
545
|
}, []);
|
|
523
546
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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]);
|
|
527
554
|
|
|
528
555
|
return (
|
|
529
556
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
@@ -543,7 +570,6 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
|
|
|
543
570
|
onCompositionStart={startComposition}
|
|
544
571
|
onKeyUp={updateSelection}
|
|
545
572
|
onInput={handleOnChangeText}
|
|
546
|
-
onSelect={handleSelectionChange}
|
|
547
573
|
onClick={handleClick}
|
|
548
574
|
onFocus={handleFocus}
|
|
549
575
|
onBlur={handleBlur}
|
|
@@ -564,8 +590,8 @@ const styles = StyleSheet.create({
|
|
|
564
590
|
// @ts-expect-error it works on web
|
|
565
591
|
boxSizing: 'border-box',
|
|
566
592
|
whiteSpace: 'pre-wrap',
|
|
567
|
-
overflowY: '
|
|
568
|
-
overflowX: '
|
|
593
|
+
overflowY: 'auto',
|
|
594
|
+
overflowX: 'auto',
|
|
569
595
|
overflowWrap: 'break-word',
|
|
570
596
|
},
|
|
571
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};
|