-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathindex.ts
155 lines (133 loc) · 5.7 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
import Parser from '@libs/Parser';
import type UseHtmlPaste from './types';
const insertByCommand = (text: string) => {
document.execCommand('insertText', false, text);
};
const insertAtCaret = (target: HTMLElement, text: string) => {
const selection = window.getSelection();
if (selection?.rangeCount) {
const range = selection.getRangeAt(0);
range.deleteContents();
const node = document.createTextNode(text);
range.insertNode(node);
// Move caret to the end of the newly inserted text node.
range.setStart(node, node.length);
range.setEnd(node, node.length);
selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
// Dispatch input event to trigger Markdown Input to parse the new text
target.dispatchEvent(new Event('input', {bubbles: true}));
} else {
insertByCommand(text);
}
};
const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false) => {
const navigation = useNavigation();
/**
* Set pasted text to clipboard
* @param {String} text
*/
const paste = useCallback((text: string) => {
try {
const textInputHTMLElement = textInputRef.current as HTMLElement;
if (textInputHTMLElement?.hasAttribute('contenteditable')) {
insertAtCaret(textInputHTMLElement, text);
} else {
insertByCommand(text);
}
// Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
// To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler
// We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered
textInputHTMLElement.dispatchEvent(
new FocusEvent('focusin', {
bubbles: true,
}),
);
// eslint-disable-next-line no-empty
} catch (e) {}
// We only need to set the callback once.
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
/**
* Manually place the pasted HTML into Composer
*
* @param {String} html - pasted HTML
*/
const handlePastedHTML = useCallback(
(html: string) => {
paste(Parser.htmlToMarkdown(html));
},
[paste],
);
/**
* Paste the plaintext content into Composer.
*
* @param {ClipboardEvent} event
*/
const handlePastePlainText = useCallback(
(event: ClipboardEvent) => {
const plainText = event.clipboardData?.getData('text/plain');
if (plainText) {
paste(plainText);
}
},
[paste],
);
const handlePaste = useCallback(
(event: ClipboardEvent) => {
if (!textInputRef.current) {
return;
}
if (preHtmlPasteCallback?.(event)) {
return;
}
const isFocused = textInputRef.current?.isFocused();
if (!isFocused) {
return;
}
event.preventDefault();
const TEXT_HTML = 'text/html';
// If paste contains HTML
if (event.clipboardData?.types?.includes(TEXT_HTML)) {
const pastedHTML = event.clipboardData.getData(TEXT_HTML);
const domparser = new DOMParser();
const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images;
// Exclude parsing img tags in the HTML, as fetching the image via fetch triggers a connect-src Content-Security-Policy error.
if (embeddedImages.length > 0 && embeddedImages[0].src) {
// If HTML has emoji, then treat this as plain text.
if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') {
handlePastePlainText(event);
return;
}
}
handlePastedHTML(pastedHTML);
return;
}
handlePastePlainText(event);
},
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[handlePastedHTML, handlePastePlainText, preHtmlPasteCallback],
);
useEffect(() => {
// we need to re-register listener on navigation focus/blur if the component (like Composer) is not unmounting
// when navigating away to different screen (report) to avoid paste event on other screen being wrongly handled
// by current screen paste listener
let unsubscribeFocus: () => void;
let unsubscribeBlur: () => void;
if (removeListenerOnScreenBlur) {
unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste, true));
unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste, true));
}
document.addEventListener('paste', handlePaste, true);
return () => {
if (removeListenerOnScreenBlur) {
unsubscribeFocus();
unsubscribeBlur();
}
document.removeEventListener('paste', handlePaste, true);
};
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
};
export default useHtmlPaste;