diff --git a/src/libs/Clipboard/index.native.js b/src/libs/Clipboard/index.native.js deleted file mode 100644 index fe79e38585c4..000000000000 --- a/src/libs/Clipboard/index.native.js +++ /dev/null @@ -1,18 +0,0 @@ -import Clipboard from '@react-native-clipboard/clipboard'; - -/** - * Sets a string on the Clipboard object via @react-native-clipboard/clipboard - * - * @param {String} text - */ -const setString = (text) => { - Clipboard.setString(text); -}; - -export default { - setString, - - // We don't want to set HTML on native platforms so noop them. - canSetHtml: () => false, - setHtml: () => {}, -}; diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts new file mode 100644 index 000000000000..f78c5e4ab230 --- /dev/null +++ b/src/libs/Clipboard/index.native.ts @@ -0,0 +1,19 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import {CanSetHtml, SetHtml, SetString} from './types'; + +/** + * Sets a string on the Clipboard object via @react-native-clipboard/clipboard + */ +const setString: SetString = (text) => { + Clipboard.setString(text); +}; + +// We don't want to set HTML on native platforms so noop them. +const canSetHtml: CanSetHtml = () => false; +const setHtml: SetHtml = () => {}; + +export default { + setString, + canSetHtml, + setHtml, +}; diff --git a/src/libs/Clipboard/index.js b/src/libs/Clipboard/index.ts similarity index 68% rename from src/libs/Clipboard/index.js rename to src/libs/Clipboard/index.ts index 3fb2091c5cb1..b703b0b4d7f5 100644 --- a/src/libs/Clipboard/index.js +++ b/src/libs/Clipboard/index.ts @@ -1,16 +1,34 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import lodashGet from 'lodash/get'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; +import {CanSetHtml, SetHtml, SetString} from './types'; -const canSetHtml = () => lodashGet(navigator, 'clipboard.write'); +type ComposerSelection = { + start: number; + end: number; + direction: 'forward' | 'backward' | 'none'; +}; + +type AnchorSelection = { + anchorOffset: number; + focusOffset: number; + anchorNode: Node; + focusNode: Node; +}; + +type NullableObject = {[K in keyof T]: T[K] | null}; + +type OriginalSelection = ComposerSelection | NullableObject; + +const canSetHtml: CanSetHtml = + () => + (...args: ClipboardItems) => + navigator?.clipboard?.write([...args]); /** * Deprecated method to write the content as HTML to clipboard. - * @param {String} html HTML representation - * @param {String} text Plain text representation */ -function setHTMLSync(html, text) { +function setHTMLSync(html: string, text: string) { const node = document.createElement('span'); node.textContent = html; node.style.all = 'unset'; @@ -21,16 +39,21 @@ function setHTMLSync(html, text) { node.addEventListener('copy', (e) => { e.stopPropagation(); e.preventDefault(); - e.clipboardData.clearData(); - e.clipboardData.setData('text/html', html); - e.clipboardData.setData('text/plain', text); + e.clipboardData?.clearData(); + e.clipboardData?.setData('text/html', html); + e.clipboardData?.setData('text/plain', text); }); document.body.appendChild(node); - const selection = window.getSelection(); - const firstAnchorChild = selection.anchorNode && selection.anchorNode.firstChild; + const selection = window?.getSelection(); + + if (selection === null) { + return; + } + + const firstAnchorChild = selection.anchorNode?.firstChild; const isComposer = firstAnchorChild instanceof HTMLTextAreaElement; - let originalSelection = null; + let originalSelection: OriginalSelection | null = null; if (isComposer) { originalSelection = { start: firstAnchorChild.selectionStart, @@ -60,12 +83,14 @@ function setHTMLSync(html, text) { selection.removeAllRanges(); - if (isComposer) { + const anchorSelection = originalSelection as AnchorSelection; + + if (isComposer && 'start' in originalSelection) { firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction); - } else if (originalSelection.anchorNode && originalSelection.focusNode) { + } else if (anchorSelection.anchorNode && anchorSelection.focusNode) { // When copying to the clipboard here, the original values of anchorNode and focusNode will be null since there will be no user selection. // We are adding a check to prevent null values from being passed to setBaseAndExtent, in accordance with the standards of the Selection API as outlined here: https://w3c.github.io/selection-api/#dom-selection-setbaseandextent. - selection.setBaseAndExtent(originalSelection.anchorNode, originalSelection.anchorOffset, originalSelection.focusNode, originalSelection.focusOffset); + selection.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); } document.body.removeChild(node); @@ -73,10 +98,8 @@ function setHTMLSync(html, text) { /** * Writes the content as HTML if the web client supports it. - * @param {String} html HTML representation - * @param {String} text Plain text representation */ -const setHtml = (html, text) => { +const setHtml: SetHtml = (html: string, text: string) => { if (!html || !text) { return; } @@ -93,8 +116,8 @@ const setHtml = (html, text) => { setHTMLSync(html, text); } else { navigator.clipboard.write([ - // eslint-disable-next-line no-undef new ClipboardItem({ + /* eslint-disable @typescript-eslint/naming-convention */ 'text/html': new Blob([html], {type: 'text/html'}), 'text/plain': new Blob([text], {type: 'text/plain'}), }), @@ -104,10 +127,8 @@ const setHtml = (html, text) => { /** * Sets a string on the Clipboard object via react-native-web - * - * @param {String} text */ -const setString = (text) => { +const setString: SetString = (text) => { Clipboard.setString(text); }; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts new file mode 100644 index 000000000000..1d899144a2ba --- /dev/null +++ b/src/libs/Clipboard/types.ts @@ -0,0 +1,5 @@ +type SetString = (text: string) => void; +type SetHtml = (html: string, text: string) => void; +type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean); + +export type {SetString, CanSetHtml, SetHtml}; diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts new file mode 100644 index 000000000000..f7db951eadad --- /dev/null +++ b/src/types/modules/react-native-web.d.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +declare module 'react-native-web' { + class Clipboard { + static isAvailable(): boolean; + static getString(): Promise; + static setString(text: string): boolean; + } + + export {Clipboard}; +}