diff --git a/src/libs/SelectionScraper/index.native.js b/src/libs/SelectionScraper/index.native.js
deleted file mode 100644
index 3872ece30b66..000000000000
--- a/src/libs/SelectionScraper/index.native.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- // This is a no-op function for native devices because they wouldn't be able to support Selection API like a website.
- getCurrentSelection: () => '',
-};
diff --git a/src/libs/SelectionScraper/index.native.ts b/src/libs/SelectionScraper/index.native.ts
new file mode 100644
index 000000000000..7712906f05e6
--- /dev/null
+++ b/src/libs/SelectionScraper/index.native.ts
@@ -0,0 +1,8 @@
+import GetCurrentSelection from './types';
+
+// This is a no-op function for native devices because they wouldn't be able to support Selection API like a website.
+const getCurrentSelection: GetCurrentSelection = () => '';
+
+export default {
+ getCurrentSelection,
+};
diff --git a/src/libs/SelectionScraper/index.js b/src/libs/SelectionScraper/index.ts
similarity index 65%
rename from src/libs/SelectionScraper/index.js
rename to src/libs/SelectionScraper/index.ts
index 02b3ff8bf61b..1f62f83e1c91 100644
--- a/src/libs/SelectionScraper/index.js
+++ b/src/libs/SelectionScraper/index.ts
@@ -1,25 +1,29 @@
import render from 'dom-serializer';
+import {DataNode, Element, Node} from 'domhandler';
import Str from 'expensify-common/lib/str';
import {parseDocument} from 'htmlparser2';
-import _ from 'underscore';
import CONST from '@src/CONST';
+import GetCurrentSelection from './types';
const elementsWillBeSkipped = ['html', 'body'];
const tagAttribute = 'data-testid';
/**
* Reads html of selection. If browser doesn't support Selection API, returns empty string.
- * @returns {String} HTML of selection as String
+ * @returns HTML of selection as String
*/
-const getHTMLOfSelection = () => {
+const getHTMLOfSelection = (): string => {
// If browser doesn't support Selection API, return an empty string.
if (!window.getSelection) {
return '';
}
const selection = window.getSelection();
+ if (!selection) {
+ return '';
+ }
if (selection.rangeCount <= 0) {
- return window.getSelection().toString();
+ return window.getSelection()?.toString() ?? '';
}
const div = document.createElement('div');
@@ -44,7 +48,7 @@ const getHTMLOfSelection = () => {
// If clonedSelection has no text content this data has no meaning to us.
if (clonedSelection.textContent) {
- let parent;
+ let parent: globalThis.Element | null = null;
let child = clonedSelection;
// If selection starts and ends within same text node we use its parentNode. This is because we can't
@@ -65,16 +69,16 @@ const getHTMLOfSelection = () => {
if (range.commonAncestorContainer instanceof HTMLElement) {
parent = range.commonAncestorContainer.closest(`[${tagAttribute}]`);
} else {
- parent = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`);
+ parent = (range.commonAncestorContainer.parentNode as HTMLElement | null)?.closest(`[${tagAttribute}]`) ?? null;
}
// Keep traversing up to clone all parents with 'data-testid' attribute.
while (parent) {
const cloned = parent.cloneNode();
cloned.appendChild(child);
- child = cloned;
+ child = cloned as DocumentFragment;
- parent = parent.parentNode.closest(`[${tagAttribute}]`);
+ parent = (parent.parentNode as HTMLElement | null)?.closest(`[${tagAttribute}]`) ?? null;
}
div.appendChild(child);
@@ -96,40 +100,41 @@ const getHTMLOfSelection = () => {
/**
* Clears all attributes from dom elements
- * @param {Object} dom htmlparser2 dom representation
- * @param {Boolean} isChildOfEditorElement
- * @returns {Object} htmlparser2 dom representation
+ * @param dom - dom htmlparser2 dom representation
*/
-const replaceNodes = (dom, isChildOfEditorElement) => {
- let domName = dom.name;
- let domChildren;
- const domAttribs = {};
- let data;
+const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => {
+ let domName;
+ let domChildren: Node[] = [];
+ const domAttribs: Element['attribs'] = {};
+ let data = '';
// Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method.
- if (dom.type === 'text') {
+ if (dom.type.toString() === 'text' && dom instanceof DataNode) {
data = Str.htmlEncode(dom.data);
- }
-
- // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data
- // has no meaning for us.
- if (dom.attribs && dom.attribs[tagAttribute]) {
- if (!elementsWillBeSkipped.includes(dom.attribs[tagAttribute])) {
- domName = dom.attribs[tagAttribute];
+ } else if (dom instanceof Element) {
+ domName = dom.name;
+ // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data
+ // has no meaning for us.
+ if (dom.attribs?.[tagAttribute]) {
+ if (!elementsWillBeSkipped.includes(dom.attribs[tagAttribute])) {
+ domName = dom.attribs[tagAttribute];
+ }
+ } else if (dom.name === 'div' && dom.children.length === 1 && isChildOfEditorElement) {
+ // We are excluding divs that are children of our editor element and have only one child to prevent
+ // additional newlines from being added in the HTML to Markdown conversion process.
+ return replaceNodes(dom.children[0], isChildOfEditorElement);
}
- } else if (dom.name === 'div' && dom.children.length === 1 && isChildOfEditorElement) {
- // We are excluding divs that are children of our editor element and have only one child to prevent
- // additional newlines from being added in the HTML to Markdown conversion process.
- return replaceNodes(dom.children[0], isChildOfEditorElement);
- }
- // We need to preserve href attribute in order to copy links.
- if (dom.attribs && dom.attribs.href) {
- domAttribs.href = dom.attribs.href;
- }
+ // We need to preserve href attribute in order to copy links.
+ if (dom.attribs?.href) {
+ domAttribs.href = dom.attribs.href;
+ }
- if (dom.children) {
- domChildren = _.map(dom.children, (c) => replaceNodes(c, isChildOfEditorElement || !_.isEmpty(dom.attribs && dom.attribs[tagAttribute])));
+ if (dom.children) {
+ domChildren = dom.children.map((c) => replaceNodes(c, isChildOfEditorElement || !!dom.attribs?.[tagAttribute]));
+ }
+ } else {
+ throw new Error(`Unknown dom type: ${dom.type}`);
}
return {
@@ -138,16 +143,15 @@ const replaceNodes = (dom, isChildOfEditorElement) => {
name: domName,
attribs: domAttribs,
children: domChildren,
- };
+ } as Element & DataNode;
};
/**
* Resolves the current selection to values and produces clean HTML.
- * @returns {String} resolved selection in the HTML format
*/
-const getCurrentSelection = () => {
+const getCurrentSelection: GetCurrentSelection = () => {
const domRepresentation = parseDocument(getHTMLOfSelection());
- domRepresentation.children = _.map(domRepresentation.children, replaceNodes);
+ domRepresentation.children = domRepresentation.children.map((item) => replaceNodes(item, false));
// Newline characters need to be removed here because the HTML could contain both newlines and
tags, and when
//
tags are converted later to markdown, it creates duplicate newline characters. This means that when the content
diff --git a/src/libs/SelectionScraper/types.ts b/src/libs/SelectionScraper/types.ts
new file mode 100644
index 000000000000..d33338883dd4
--- /dev/null
+++ b/src/libs/SelectionScraper/types.ts
@@ -0,0 +1,3 @@
+type GetCurrentSelection = () => string;
+
+export default GetCurrentSelection;