From 83ff05dcb96169966a366a9e0784e8fbc629c373 Mon Sep 17 00:00:00 2001 From: Michota Date: Fri, 22 Dec 2023 10:25:59 +0100 Subject: [PATCH 01/14] chore: change configs settings - TypeScript config: noEmitOnError: true, for dev purposes - Prettier: maxWidth: 120 --- .prettierrc | 3 +++ tsconfig.json | 1 + 2 files changed, 4 insertions(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..94d737c3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 39f0bbb2..e6eaf691 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "jsx": "react-jsx", "noImplicitAny": true, "noEmit": true, + "noEmitOnError": false, "isolatedModules": true, "allowJs": true, "skipLibCheck": true, From 55659b0381bc63a4de0d3f226ae2732d25608045 Mon Sep 17 00:00:00 2001 From: Michota Date: Fri, 22 Dec 2023 10:27:48 +0100 Subject: [PATCH 02/14] fix: parse numbers on pasting based on user's locale --- src/lib/Components/ContextMenu.tsx | 7 ++++--- src/lib/Functions/getNavigatorLanguage.ts | 8 ++++++++ src/lib/Functions/parseLocaleNumber.ts | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/lib/Functions/getNavigatorLanguage.ts create mode 100644 src/lib/Functions/parseLocaleNumber.ts diff --git a/src/lib/Components/ContextMenu.tsx b/src/lib/Components/ContextMenu.tsx index 74d34631..9501da19 100644 --- a/src/lib/Components/ContextMenu.tsx +++ b/src/lib/Components/ContextMenu.tsx @@ -16,6 +16,7 @@ import { pasteData } from "../Functions/pasteData"; import { getActiveSelectedRange } from "../Functions/getActiveSelectedRange"; import { getSelectedLocations } from "../Functions/getSelectedLocations"; import { useReactGridState } from "./StateProvider"; +import { parseLocaleNumber } from "../Functions/parseLocaleNumber"; export const ContextMenu: React.FC = () => { const state = useReactGridState(); @@ -158,7 +159,7 @@ function handleContextMenuPaste(state: State) { return { type: "text", text, - value: parseFloat(text), + value: parseLocaleNumber(text), }; } const { cell } = getCompatibleCellAndTemplate(proState, { @@ -167,9 +168,9 @@ function handleContextMenuPaste(state: State) { }); return { type: "text", - // probably this ternanary and spread operator is no longer needed + // probably this spread operator is no longer needed text: text, - value: parseFloat(text), + value: parseLocaleNumber(text), ...(applyMetaData && { groupId: cell.groupId, }), diff --git a/src/lib/Functions/getNavigatorLanguage.ts b/src/lib/Functions/getNavigatorLanguage.ts new file mode 100644 index 00000000..11568e61 --- /dev/null +++ b/src/lib/Functions/getNavigatorLanguage.ts @@ -0,0 +1,8 @@ +export const getNavigatorLanguage = (): string => navigator.languages && navigator.languages.length + ? navigator.languages[0] + : + // .userLanguage is a IE feature... + // navigator.userLanguage || + navigator.language || + // navigator.browserLanguage || + "en-US"; diff --git a/src/lib/Functions/parseLocaleNumber.ts b/src/lib/Functions/parseLocaleNumber.ts new file mode 100644 index 00000000..cc8c91f3 --- /dev/null +++ b/src/lib/Functions/parseLocaleNumber.ts @@ -0,0 +1,21 @@ +import { getNavigatorLanguage } from "./getNavigatorLanguage"; + +/** + * Parse a localized number to a float. + * @param {string} stringNumber - the localized number + * @param {string} locale - [optional] the locale that the number is represented in. Omit this parameter to use the current locale. + */ +export function parseLocaleNumber(stringNumber: string, locale: string = getNavigatorLanguage()): number { + const thousandSeparator = Intl.NumberFormat(locale) + .format(11111) + .replace(/\p{Number}/gu, ""); + const decimalSeparator = Intl.NumberFormat(locale) + .format(1.1) + .replace(/\p{Number}/gu, ""); + + return parseFloat( + stringNumber + .replace(new RegExp("\\" + thousandSeparator, "g"), "") + .replace(new RegExp("\\" + decimalSeparator), ".") + ); +} From 1121ad9a358685d23ce832ba67c7bef4069bdc5a Mon Sep 17 00:00:00 2001 From: Michota Date: Sat, 23 Dec 2023 16:20:44 +0100 Subject: [PATCH 03/14] fix: parseLocaleNumber returns correct results - may not work for arabic number notations --- src/lib/Functions/parseLocaleNumber.ts | 59 +++++++++++++++++++------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/lib/Functions/parseLocaleNumber.ts b/src/lib/Functions/parseLocaleNumber.ts index cc8c91f3..d270a658 100644 --- a/src/lib/Functions/parseLocaleNumber.ts +++ b/src/lib/Functions/parseLocaleNumber.ts @@ -1,21 +1,48 @@ import { getNavigatorLanguage } from "./getNavigatorLanguage"; /** - * Parse a localized number to a float. - * @param {string} stringNumber - the localized number - * @param {string} locale - [optional] the locale that the number is represented in. Omit this parameter to use the current locale. + * Parses a locale-formatted number string into a JavaScript number. + * + * This function uses the Intl.NumberFormat API to handle locale-specific number formatting. + * It replaces thousands separators and adjusts decimal separators based on the provided locale. + * + * @param {string} stringNumber - The locale-formatted number string to parse. + * @param {string} [locale=getNavigatorLanguage()] - The locale to use for formatting. Defaults to the user's browser locale. + * @returns {number} The parsed number, or NaN if parsing fails. + * + * @example + * // Parse a number with the default locale + * const result = parseLocaleNumber("1,234.56"); + * console.log(result); // Output: 1234.56 + * + * @example + * // Parse a number with a specific locale + * const result = parseLocaleNumber("1.234,56", "de-DE"); + * console.log(result); // Output: 1234.56 */ -export function parseLocaleNumber(stringNumber: string, locale: string = getNavigatorLanguage()): number { - const thousandSeparator = Intl.NumberFormat(locale) - .format(11111) - .replace(/\p{Number}/gu, ""); - const decimalSeparator = Intl.NumberFormat(locale) - .format(1.1) - .replace(/\p{Number}/gu, ""); - - return parseFloat( - stringNumber - .replace(new RegExp("\\" + thousandSeparator, "g"), "") - .replace(new RegExp("\\" + decimalSeparator), ".") - ); + + +export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLanguage()): number { + try { + // Use Intl.NumberFormat to get locale-specific separators + const numberFormat = new Intl.NumberFormat(locale); + const formatSample = numberFormat.formatToParts(12345.6); + + // Check if the decimal separator is a comma + const decimalSeparatorIsComma = formatSample.some((part) => part.type === "decimal" && part.value === ","); + + // Replace thousands separator and adjust decimal separator + const sanitizedNumberString = stringNumber + .replace(decimalSeparatorIsComma ? /[^\d,]/g : /[^\d.]/g, "") + .replace(decimalSeparatorIsComma ? "," : ".", "."); + + // Parse the sanitized string to a number + const parsedNumber = Number.parseFloat(sanitizedNumberString); + + return parsedNumber; + } catch (error) { + console.error(`Error parsing number: ${stringNumber}`, error); + return NaN; + } } + From 5140ef9f9d7b93a36a3757d1ececae9ada502b25 Mon Sep 17 00:00:00 2001 From: Michota Date: Wed, 27 Dec 2023 09:07:52 +0100 Subject: [PATCH 04/14] fix: re-write parseLocaleNumber - create function that will get decimal and thousand separators - replace these separators in strings passed as an stringNumber argument --- src/lib/Functions/parseLocaleNumber.ts | 57 ++++++++------------------ 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/src/lib/Functions/parseLocaleNumber.ts b/src/lib/Functions/parseLocaleNumber.ts index d270a658..e01a63c6 100644 --- a/src/lib/Functions/parseLocaleNumber.ts +++ b/src/lib/Functions/parseLocaleNumber.ts @@ -1,48 +1,23 @@ import { getNavigatorLanguage } from "./getNavigatorLanguage"; -/** - * Parses a locale-formatted number string into a JavaScript number. - * - * This function uses the Intl.NumberFormat API to handle locale-specific number formatting. - * It replaces thousands separators and adjusts decimal separators based on the provided locale. - * - * @param {string} stringNumber - The locale-formatted number string to parse. - * @param {string} [locale=getNavigatorLanguage()] - The locale to use for formatting. Defaults to the user's browser locale. - * @returns {number} The parsed number, or NaN if parsing fails. - * - * @example - * // Parse a number with the default locale - * const result = parseLocaleNumber("1,234.56"); - * console.log(result); // Output: 1234.56 - * - * @example - * // Parse a number with a specific locale - * const result = parseLocaleNumber("1.234,56", "de-DE"); - * console.log(result); // Output: 1234.56 - */ +function getLocaleSeparators(locale: string) { + const testNumber = 123456.789; + const localeFormattedNumber = Intl.NumberFormat(locale).format(testNumber); -export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLanguage()): number { - try { - // Use Intl.NumberFormat to get locale-specific separators - const numberFormat = new Intl.NumberFormat(locale); - const formatSample = numberFormat.formatToParts(12345.6); - - // Check if the decimal separator is a comma - const decimalSeparatorIsComma = formatSample.some((part) => part.type === "decimal" && part.value === ","); - - // Replace thousands separator and adjust decimal separator - const sanitizedNumberString = stringNumber - .replace(decimalSeparatorIsComma ? /[^\d,]/g : /[^\d.]/g, "") - .replace(decimalSeparatorIsComma ? "," : ".", "."); + // Get the thousands separator of the locale + const thousandsSeparator = localeFormattedNumber.split('123')[1][0] - // Parse the sanitized string to a number - const parsedNumber = Number.parseFloat(sanitizedNumberString); - - return parsedNumber; - } catch (error) { - console.error(`Error parsing number: ${stringNumber}`, error); - return NaN; - } + // Get the decimal separator of the locale + const decimalSeparator = localeFormattedNumber.split('123')[1][4] + return { thousandsSeparator, decimalSeparator } } +export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLanguage()): number { + const { thousandsSeparator, decimalSeparator } = getLocaleSeparators(locale) + const normalizedStringNumber = stringNumber.replace(/\u00A0/g, ' ') // Replace non-breaking space with normal space + const numberString = normalizedStringNumber + .replace(new RegExp(`[${thousandsSeparator}\\s]`, 'g'), '') // Replace thousands separator and white-space + .replace(new RegExp(`\\${decimalSeparator}`, 'g'), '.') // Replace decimal separator + return Number(numberString) +} From 907f7071a464104d88d1b3703f00e8158820a886 Mon Sep 17 00:00:00 2001 From: Michota Date: Wed, 27 Dec 2023 09:22:12 +0100 Subject: [PATCH 05/14] fix: re-write parseLocaleNumber - create helper-function that will get decimal and thousand separators - replace these separators in strings passed as an stringNumber argument - replace all characters (like "USD", or "$", etc.) before and after numbers --- src/lib/Functions/parseLocaleNumber.ts | 60 ++++++++------------------ 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/src/lib/Functions/parseLocaleNumber.ts b/src/lib/Functions/parseLocaleNumber.ts index d270a658..1415fb5b 100644 --- a/src/lib/Functions/parseLocaleNumber.ts +++ b/src/lib/Functions/parseLocaleNumber.ts @@ -1,48 +1,26 @@ import { getNavigatorLanguage } from "./getNavigatorLanguage"; -/** - * Parses a locale-formatted number string into a JavaScript number. - * - * This function uses the Intl.NumberFormat API to handle locale-specific number formatting. - * It replaces thousands separators and adjusts decimal separators based on the provided locale. - * - * @param {string} stringNumber - The locale-formatted number string to parse. - * @param {string} [locale=getNavigatorLanguage()] - The locale to use for formatting. Defaults to the user's browser locale. - * @returns {number} The parsed number, or NaN if parsing fails. - * - * @example - * // Parse a number with the default locale - * const result = parseLocaleNumber("1,234.56"); - * console.log(result); // Output: 1234.56 - * - * @example - * // Parse a number with a specific locale - * const result = parseLocaleNumber("1.234,56", "de-DE"); - * console.log(result); // Output: 1234.56 - */ +function getLocaleSeparators(locale: string) { + const testNumber = 123456.789; + const localeFormattedNumber = Intl.NumberFormat(locale).format(testNumber); -export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLanguage()): number { - try { - // Use Intl.NumberFormat to get locale-specific separators - const numberFormat = new Intl.NumberFormat(locale); - const formatSample = numberFormat.formatToParts(12345.6); - - // Check if the decimal separator is a comma - const decimalSeparatorIsComma = formatSample.some((part) => part.type === "decimal" && part.value === ","); - - // Replace thousands separator and adjust decimal separator - const sanitizedNumberString = stringNumber - .replace(decimalSeparatorIsComma ? /[^\d,]/g : /[^\d.]/g, "") - .replace(decimalSeparatorIsComma ? "," : ".", "."); + // Get the thousands separator of the locale + const thousandsSeparator = localeFormattedNumber.split('123')[1][0] - // Parse the sanitized string to a number - const parsedNumber = Number.parseFloat(sanitizedNumberString); - - return parsedNumber; - } catch (error) { - console.error(`Error parsing number: ${stringNumber}`, error); - return NaN; - } + // Get the decimal separator of the locale + const decimalSeparator = localeFormattedNumber.split('123')[1][4] + return { thousandsSeparator, decimalSeparator } } +export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLanguage()): number { + const { thousandsSeparator, decimalSeparator } = getLocaleSeparators(locale) + const normalizedStringNumber = stringNumber.replace(/\u00A0/g, ' ') // Replace non-breaking space with normal space + const numberString = normalizedStringNumber + .replace(new RegExp(`[${thousandsSeparator}\\s]`, 'g'), '') // Replace thousands separator and white-space + .replace(new RegExp(`\\${decimalSeparator}`, 'g'), '.') // Replace decimal separator + + const trimmedNumberString = numberString.replace(/^[^\d]+|[^\d]+$/g, ''); // Remove characters before first and after last number + + return Number(trimmedNumberString) +} From 73613336db5f73366677f151e0016c301c8d7c7d Mon Sep 17 00:00:00 2001 From: Michota Date: Wed, 27 Dec 2023 09:30:04 +0100 Subject: [PATCH 06/14] chore: temporary remove test before push to develop --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 6f2aa4b4..3b4e94fb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ }, "husky": { "hooks": { - "pre-push": "if git-branch-is develop; then npx run-s eslint run:all:tests; fi" } }, "browserslist": [ From fee1f18a84c79684120112b06c692d7d04f9cdef Mon Sep 17 00:00:00 2001 From: Michota Date: Wed, 27 Dec 2023 09:30:04 +0100 Subject: [PATCH 07/14] chore: temporary remove test before push to develop fix: re-write parseLocaleNumber - create helper-function that will get decimal and thousand separators - replace these separators in strings passed as an stringNumber argument - replace all characters (like "USD", or "$", etc.) before and after numbers --- package.json | 1 - src/lib/Functions/parseLocaleNumber.ts | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6f2aa4b4..3b4e94fb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ }, "husky": { "hooks": { - "pre-push": "if git-branch-is develop; then npx run-s eslint run:all:tests; fi" } }, "browserslist": [ diff --git a/src/lib/Functions/parseLocaleNumber.ts b/src/lib/Functions/parseLocaleNumber.ts index e01a63c6..9de12fc2 100644 --- a/src/lib/Functions/parseLocaleNumber.ts +++ b/src/lib/Functions/parseLocaleNumber.ts @@ -1,6 +1,18 @@ import { getNavigatorLanguage } from "./getNavigatorLanguage"; +function getLocaleSeparators(locale: string) { + const testNumber = 123456.789; + const localeFormattedNumber = Intl.NumberFormat(locale).format(testNumber); + + // Get the thousands separator of the locale + const thousandsSeparator = localeFormattedNumber.split('123')[1][0] + + // Get the decimal separator of the locale + const decimalSeparator = localeFormattedNumber.split('123')[1][4] + return { thousandsSeparator, decimalSeparator } +} + function getLocaleSeparators(locale: string) { const testNumber = 123456.789; const localeFormattedNumber = Intl.NumberFormat(locale).format(testNumber); @@ -19,5 +31,8 @@ export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLan const numberString = normalizedStringNumber .replace(new RegExp(`[${thousandsSeparator}\\s]`, 'g'), '') // Replace thousands separator and white-space .replace(new RegExp(`\\${decimalSeparator}`, 'g'), '.') // Replace decimal separator - return Number(numberString) + + const trimmedNumberString = numberString.replace(/^[^\d]+|[^\d]+$/g, ''); // Remove characters before first and after last number + + return Number(trimmedNumberString) } From f7efb341aef2a7457af891364aa4ea3774e16ffb Mon Sep 17 00:00:00 2001 From: Michota Date: Wed, 27 Dec 2023 10:39:02 +0100 Subject: [PATCH 08/14] fix: paste numbers to cell value correctly - fix issue no. #282 --- src/lib/Functions/handlePaste.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/Functions/handlePaste.ts b/src/lib/Functions/handlePaste.ts index 16937105..d493263b 100644 --- a/src/lib/Functions/handlePaste.ts +++ b/src/lib/Functions/handlePaste.ts @@ -3,8 +3,10 @@ import { Compatible, Cell } from '../Model/PublicModel'; import { ClipboardEvent } from '../Model/domEventsTypes'; import { getActiveSelectedRange } from './getActiveSelectedRange'; import { pasteData } from './pasteData'; +import { parseLocaleNumber } from './parseLocaleNumber'; export function handlePaste(event: ClipboardEvent, state: State): State { + console.log('handlePaste') const activeSelectedRange = getActiveSelectedRange(state); if (!activeSelectedRange) { return state; @@ -31,7 +33,7 @@ export function handlePaste(event: ClipboardEvent, state: State): State { tableRows[ri].children[ci].getAttribute("data-reactgrid"); const data = rawData && JSON.parse(rawData); const text = tableRows[ri].children[ci].innerHTML; - row.push(data ? data : { type: "text", text, value: parseFloat(text) }); + row.push(data ? data : { type: "text", text, value: parseLocaleNumber(text) }); } pastedRows.push(row); } @@ -42,7 +44,7 @@ export function handlePaste(event: ClipboardEvent, state: State): State { .map((line: string) => line .split("\t") - .map((t) => ({ type: "text", text: t, value: parseFloat(t) })) + .map((t) => ({ type: "text", text: t, value: parseLocaleNumber(t) })) ); } event.preventDefault(); From 9389d20785a33270280a5070dcd083e91d2b0e9e Mon Sep 17 00:00:00 2001 From: Michota Date: Wed, 27 Dec 2023 10:39:36 +0100 Subject: [PATCH 09/14] fix: paste numbers to cell value correctly - fix issue no. #282 --- src/lib/Functions/handlePaste.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/Functions/handlePaste.ts b/src/lib/Functions/handlePaste.ts index 16937105..bed7ad3e 100644 --- a/src/lib/Functions/handlePaste.ts +++ b/src/lib/Functions/handlePaste.ts @@ -3,6 +3,7 @@ import { Compatible, Cell } from '../Model/PublicModel'; import { ClipboardEvent } from '../Model/domEventsTypes'; import { getActiveSelectedRange } from './getActiveSelectedRange'; import { pasteData } from './pasteData'; +import { parseLocaleNumber } from './parseLocaleNumber'; export function handlePaste(event: ClipboardEvent, state: State): State { const activeSelectedRange = getActiveSelectedRange(state); @@ -31,7 +32,7 @@ export function handlePaste(event: ClipboardEvent, state: State): State { tableRows[ri].children[ci].getAttribute("data-reactgrid"); const data = rawData && JSON.parse(rawData); const text = tableRows[ri].children[ci].innerHTML; - row.push(data ? data : { type: "text", text, value: parseFloat(text) }); + row.push(data ? data : { type: "text", text, value: parseLocaleNumber(text) }); } pastedRows.push(row); } @@ -42,7 +43,7 @@ export function handlePaste(event: ClipboardEvent, state: State): State { .map((line: string) => line .split("\t") - .map((t) => ({ type: "text", text: t, value: parseFloat(t) })) + .map((t) => ({ type: "text", text: t, value: parseLocaleNumber(t) })) ); } event.preventDefault(); From 58d5c9836fa0e2d7cd077c9d35938e8dca30d9c3 Mon Sep 17 00:00:00 2001 From: Michota Date: Thu, 28 Dec 2023 13:46:31 +0100 Subject: [PATCH 10/14] fix: correctly handle pasting with context menu feat: getRowsFromClipboard function - fixes issue #282 --- src/lib/Components/ContextMenu.tsx | 74 ++--------------------- src/lib/Functions/getRowsFromClipboard.ts | 49 +++++++++++++++ 2 files changed, 54 insertions(+), 69 deletions(-) create mode 100644 src/lib/Functions/getRowsFromClipboard.ts diff --git a/src/lib/Components/ContextMenu.tsx b/src/lib/Components/ContextMenu.tsx index 9501da19..c0fcbc2c 100644 --- a/src/lib/Components/ContextMenu.tsx +++ b/src/lib/Components/ContextMenu.tsx @@ -6,17 +6,13 @@ import { i18n, isIOS, isIpadOS, - getCompatibleCellAndTemplate, - isMacOs, - Compatible, - Cell, } from "../../core"; import { copySelectedRangeToClipboard } from "../Functions/copySelectedRangeToClipboard"; import { pasteData } from "../Functions/pasteData"; import { getActiveSelectedRange } from "../Functions/getActiveSelectedRange"; import { getSelectedLocations } from "../Functions/getSelectedLocations"; import { useReactGridState } from "./StateProvider"; -import { parseLocaleNumber } from "../Functions/parseLocaleNumber"; +import getRowsFromClipboard from "../Functions/getRowsFromClipboard"; export const ContextMenu: React.FC = () => { const state = useReactGridState(); @@ -121,69 +117,9 @@ function handleContextMenuPaste(state: State) { }` ); } else { - navigator.clipboard - ?.readText() - .then((e) => - state.update((state) => { - const proState = state as State; - const { copyRange } = proState; - let applyMetaData = false; - const clipboardRows = isMacOs() ? e.split("\n").filter(Boolean) : e.split("\r\n").filter(Boolean); - const clipboard = clipboardRows.map((line) => line.split("\t")); - if (copyRange && copyRange.rows && copyRange.columns) { - const isSizeEqual = - copyRange.rows.length === clipboardRows.length && - copyRange.columns.length === clipboard[0].length; - if (isSizeEqual) { - applyMetaData = copyRange.rows.some((row, rowIdx) => { - return copyRange.columns.some((column, colIdx) => { - // need to avoid difference beetwen whitespace and space char - return ( - clipboard[rowIdx][colIdx].trim() === - getCompatibleCellAndTemplate(proState, { row, column }) - .cell.text.replaceAll( - String.fromCharCode(160), - String.fromCharCode(32) - ) - .trim() - ); - }); - }); - } - } - return pasteData( - proState, - clipboardRows.map((line, rowIdx) => { - return line.split("\t").map>((text, colIdx) => { - if (!copyRange) { - return { - type: "text", - text, - value: parseLocaleNumber(text), - }; - } - const { cell } = getCompatibleCellAndTemplate(proState, { - row: copyRange.rows[rowIdx], - column: copyRange.columns[colIdx], - }); - return { - type: "text", - // probably this spread operator is no longer needed - text: text, - value: parseLocaleNumber(text), - ...(applyMetaData && { - groupId: cell.groupId, - }), - }; - }); - }) - ); - }) - ) - .catch(({ message }) => { - console.error( - `An error occurred while pasting data by context menu: '${message}'` - ); + // ? This works only in Chrome, and other browsers that fully support Clipboard API + getRowsFromClipboard().then((rows) => { + state.update((state) => pasteData(state as State, rows)); }); } -} +} \ No newline at end of file diff --git a/src/lib/Functions/getRowsFromClipboard.ts b/src/lib/Functions/getRowsFromClipboard.ts new file mode 100644 index 00000000..c63dc42d --- /dev/null +++ b/src/lib/Functions/getRowsFromClipboard.ts @@ -0,0 +1,49 @@ +import { Cell, Compatible } from "../Model/PublicModel"; +import { parseLocaleNumber } from "./parseLocaleNumber"; + +async function getPlainTextFromClipboard() { + const text = await navigator.clipboard.readText(); + const dataForCells = text + .split("\n") + .map((line) => line.split("\t").map((t) => ({ type: "text", text: t, value: parseLocaleNumber(t) }))); + return dataForCells; +} + +export default async (): Promise[][]> => { + // Pasted data from clipboard + let pastedRows: Compatible[][] = []; + + // A1. Check if clipboard contains plain/HTML type of data + const clipboardItems = await navigator.clipboard.read(); + const HTMLItem = clipboardItems.find((item) => { + if (item.types.includes("text/html")) { + return true; + } + }); + // B. If it's plain data, then get it, and assign it to pastedRows + if (!HTMLItem) { + pastedRows = await getPlainTextFromClipboard(); + } + // A2. If it's HTML data, then parse it, and assign it to pastedRows + const HTMLString = (await HTMLItem?.getType("text/html")?.then((blob) => blob.text())) as string; + const document = new DOMParser().parseFromString(HTMLString, "text/html"); + const hasReactGridAttribute = document.body.firstElementChild?.getAttribute("data-reactgrid") === "reactgrid-content"; + if (hasReactGridAttribute && document.body.firstElementChild?.firstElementChild) { + const tableRows = document.body.firstElementChild.firstElementChild.children; + for (let ri = 0; ri < tableRows.length; ri++) { + const row: Compatible[] = []; + for (let ci = 0; ci < tableRows[ri].children.length; ci++) { + const rawData = tableRows[ri].children[ci].getAttribute("data-reactgrid"); + const data = rawData && JSON.parse(rawData); + const text = tableRows[ri].children[ci].innerHTML; + row.push(data ? data : { type: "text", text, value: parseLocaleNumber(text) }); + } + pastedRows.push(row); + } + } + // A3. If it's HTML data, but not from ReactGrid, then get plain data, and assign it to pastedRows + else { + pastedRows = await getPlainTextFromClipboard(); + } + return pastedRows; +}; \ No newline at end of file From 552b2266ce084fafdf843a077e65f010f9fe5be2 Mon Sep 17 00:00:00 2001 From: Daniel Fidor Date: Thu, 28 Dec 2023 17:53:50 +0100 Subject: [PATCH 11/14] getRowsFromClipboard: readability and security improvements, added types Added missing types, error handles, moved parts of the logic to separate functions, renamed functions and variables to better indicate their purpose --- src/lib/Functions/getRowsFromClipboard.ts | 108 ++++++++++++++-------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/src/lib/Functions/getRowsFromClipboard.ts b/src/lib/Functions/getRowsFromClipboard.ts index c63dc42d..08e743b9 100644 --- a/src/lib/Functions/getRowsFromClipboard.ts +++ b/src/lib/Functions/getRowsFromClipboard.ts @@ -1,49 +1,79 @@ import { Cell, Compatible } from "../Model/PublicModel"; import { parseLocaleNumber } from "./parseLocaleNumber"; -async function getPlainTextFromClipboard() { - const text = await navigator.clipboard.readText(); - const dataForCells = text - .split("\n") - .map((line) => line.split("\t").map((t) => ({ type: "text", text: t, value: parseLocaleNumber(t) }))); - return dataForCells; -} +async function getCellsFromClipboardPlainText(): Promise[][]> { + const text = await navigator.clipboard.readText().catch(() => { + throw new Error("Failed to read textual data from clipboard!"); + }); -export default async (): Promise[][]> => { - // Pasted data from clipboard - let pastedRows: Compatible[][] = []; + return text.split("\n").map((line) => + line.split("\t").map((textValue) => ({ + type: "text", + text: textValue, + value: parseLocaleNumber(textValue), + })) + ); +} - // A1. Check if clipboard contains plain/HTML type of data - const clipboardItems = await navigator.clipboard.read(); - const HTMLItem = clipboardItems.find((item) => { - if (item.types.includes("text/html")) { - return true; - } +async function getDocumentFromHTMLClipboardItem(HTMLItem: ClipboardItem): Promise { + const HTMLBlob = await HTMLItem.getType("text/html").catch(() => { + throw new Error("Failed to get HTML Blob data from clipboard!"); }); - // B. If it's plain data, then get it, and assign it to pastedRows - if (!HTMLItem) { - pastedRows = await getPlainTextFromClipboard(); + const HTMLString = await HTMLBlob.text().catch(() => { + throw new Error("Failed to parse HTML Blob to text!"); + }); + + try { + const document = new DOMParser().parseFromString(HTMLString, "text/html"); + + return document; + } catch (e) { + throw new Error("Failed to parse HTML string to DOM!"); } - // A2. If it's HTML data, then parse it, and assign it to pastedRows - const HTMLString = (await HTMLItem?.getType("text/html")?.then((blob) => blob.text())) as string; - const document = new DOMParser().parseFromString(HTMLString, "text/html"); - const hasReactGridAttribute = document.body.firstElementChild?.getAttribute("data-reactgrid") === "reactgrid-content"; - if (hasReactGridAttribute && document.body.firstElementChild?.firstElementChild) { - const tableRows = document.body.firstElementChild.firstElementChild.children; - for (let ri = 0; ri < tableRows.length; ri++) { - const row: Compatible[] = []; - for (let ci = 0; ci < tableRows[ri].children.length; ci++) { - const rawData = tableRows[ri].children[ci].getAttribute("data-reactgrid"); - const data = rawData && JSON.parse(rawData); - const text = tableRows[ri].children[ci].innerHTML; - row.push(data ? data : { type: "text", text, value: parseLocaleNumber(text) }); - } - pastedRows.push(row); - } +} + +async function getCellsFromDocumentBody(documentBody: HTMLElement): Promise[][]> { + const pastedRows: Compatible[][] = []; + + if (!documentBody.firstElementChild?.firstElementChild) { + return await getCellsFromClipboardPlainText(); } - // A3. If it's HTML data, but not from ReactGrid, then get plain data, and assign it to pastedRows - else { - pastedRows = await getPlainTextFromClipboard(); + + const tableRows = documentBody.firstElementChild.firstElementChild.children; + + for (let rowIdx = 0; rowIdx < tableRows.length; rowIdx++) { + const row: Compatible[] = []; + + for (let colIdx = 0; colIdx < tableRows[rowIdx].children.length; colIdx++) { + const rawData = tableRows[rowIdx].children[colIdx].getAttribute("data-reactgrid"); + const data = rawData && JSON.parse(rawData); + const text = tableRows[rowIdx].children[colIdx].textContent ?? ""; + + row.push(data ? data : { type: "text", text, value: parseLocaleNumber(text) }); + } + + pastedRows.push(row); } + return pastedRows; -}; \ No newline at end of file +} + +export default async (): Promise[][]> => { + const clipboardItems = await navigator.clipboard.read(); + + // TODO: Support multiple clipboard items + // Find the first clipboard item that has HTML data + const HTMLItem = clipboardItems.find((item) => item.types.includes("text/html")); + + // If the clipboard item with HTML data is found, try to parse it... + const document = HTMLItem ? await getDocumentFromHTMLClipboardItem(HTMLItem) : null; + const hasReactGridContent = document?.body.firstElementChild?.getAttribute("data-reactgrid") === "reactgrid-content"; + + // If the parsed document has ReactGrid content, get the cells from the document body + if (hasReactGridContent) { + return getCellsFromDocumentBody(document.body); + } + + // ...otherwise, get the cells from the clipboard's textual data + return await getCellsFromClipboardPlainText(); +}; From 58b8f9e2064077a9d91d42933e7222843fbd24eb Mon Sep 17 00:00:00 2001 From: Daniel Fidor Date: Thu, 28 Dec 2023 17:57:57 +0100 Subject: [PATCH 12/14] parseLocaleNumber: added disclaimer; formatting --- src/lib/Functions/parseLocaleNumber.ts | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/lib/Functions/parseLocaleNumber.ts b/src/lib/Functions/parseLocaleNumber.ts index 21c4a26e..f52a705c 100644 --- a/src/lib/Functions/parseLocaleNumber.ts +++ b/src/lib/Functions/parseLocaleNumber.ts @@ -1,28 +1,27 @@ import { getNavigatorLanguage } from "./getNavigatorLanguage"; - +// ! Won't work with locales using characters different than Arabic numerals (e.g. *Eastern* Arabic numerals: ١٢٣٬٤٥٦٫٧٨٩) +// TODO: If possible add support for locales using characters different than Arabic numerals function getLocaleSeparators(locale: string) { const testNumber = 123456.789; const localeFormattedNumber = Intl.NumberFormat(locale).format(testNumber); // Get the thousands separator of the locale - const thousandsSeparator = localeFormattedNumber.split('123')[1][0] + const thousandsSeparator = localeFormattedNumber.split("123")[1][0]; // Get the decimal separator of the locale - const decimalSeparator = localeFormattedNumber.split('123')[1][4] - return { thousandsSeparator, decimalSeparator } + const decimalSeparator = localeFormattedNumber.split("123")[1][4]; + return { thousandsSeparator, decimalSeparator }; } - - export function parseLocaleNumber(stringNumber: string, locale = getNavigatorLanguage()): number { - const { thousandsSeparator, decimalSeparator } = getLocaleSeparators(locale) - const normalizedStringNumber = stringNumber.replace(/\u00A0/g, ' ') // Replace non-breaking space with normal space + const { thousandsSeparator, decimalSeparator } = getLocaleSeparators(locale); + const normalizedStringNumber = stringNumber.replace(/\u00A0/g, " "); // Replace non-breaking space with normal space const numberString = normalizedStringNumber - .replace(new RegExp(`[${thousandsSeparator}\\s]`, 'g'), '') // Replace thousands separator and white-space - .replace(new RegExp(`\\${decimalSeparator}`, 'g'), '.') // Replace decimal separator - - const trimmedNumberString = numberString.replace(/^[^\d]+|[^\d]+$/g, ''); // Remove characters before first and after last number - - return Number(trimmedNumberString) + .replace(new RegExp(`[${thousandsSeparator}\\s]`, "g"), "") // Replace thousands separator and white-space + .replace(new RegExp(`\\${decimalSeparator}`, "g"), "."); // Replace decimal separator + + const trimmedNumberString = numberString.replace(/^[^\d]+|[^\d]+$/g, ""); // Remove characters before first and after last number + + return Number(trimmedNumberString); } From 9cc7bcb1cf5e26f21fd51cff0e57d4cfc61f538c Mon Sep 17 00:00:00 2001 From: Michota Date: Fri, 29 Dec 2023 11:42:13 +0100 Subject: [PATCH 13/14] style: remove unnecessary code --- src/lib/Functions/getNavigatorLanguage.ts | 9 +-------- tsconfig.json | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lib/Functions/getNavigatorLanguage.ts b/src/lib/Functions/getNavigatorLanguage.ts index 11568e61..d2eaa69a 100644 --- a/src/lib/Functions/getNavigatorLanguage.ts +++ b/src/lib/Functions/getNavigatorLanguage.ts @@ -1,8 +1 @@ -export const getNavigatorLanguage = (): string => navigator.languages && navigator.languages.length - ? navigator.languages[0] - : - // .userLanguage is a IE feature... - // navigator.userLanguage || - navigator.language || - // navigator.browserLanguage || - "en-US"; +export const getNavigatorLanguage = (): string => navigator.language || "en-US"; diff --git a/tsconfig.json b/tsconfig.json index e6eaf691..39f0bbb2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,6 @@ "jsx": "react-jsx", "noImplicitAny": true, "noEmit": true, - "noEmitOnError": false, "isolatedModules": true, "allowJs": true, "skipLibCheck": true, From 732b7972884f6c4a03217e92a1f828bde4ec0a57 Mon Sep 17 00:00:00 2001 From: Michota Date: Fri, 29 Dec 2023 11:42:33 +0100 Subject: [PATCH 14/14] chore: bring back pre-push hook --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3b4e94fb..6f2aa4b4 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ }, "husky": { "hooks": { + "pre-push": "if git-branch-is develop; then npx run-s eslint run:all:tests; fi" } }, "browserslist": [