From 8ce3ffaff89b41cfc4176bac40f76dc66a33ad5c Mon Sep 17 00:00:00 2001 From: Paul-Joel Date: Wed, 22 May 2024 10:58:59 +0100 Subject: [PATCH 1/5] Fix to RTField paste --- .../utils/createQuestionnaireIntroduction.js | 2 +- eq-author/package.json | 1 + .../src/components/RichTextEditor/index.js | 76 +++++++++++++------ eq-author/yarn.lock | 25 ++++++ 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/eq-author-api/utils/createQuestionnaireIntroduction.js b/eq-author-api/utils/createQuestionnaireIntroduction.js index dd3a3f5bfd..4ff9cc5371 100644 --- a/eq-author-api/utils/createQuestionnaireIntroduction.js +++ b/eq-author-api/utils/createQuestionnaireIntroduction.js @@ -23,7 +23,7 @@ module.exports = (metadata) => { additionalGuidancePanelSwitch: false, additionalGuidancePanel: "", description: - "", + "", legalBasis: NOTICE_1, // TODO: previewQuestions previewQuestions: false, diff --git a/eq-author/package.json b/eq-author/package.json index 2e3b4adf90..1562f6ab95 100644 --- a/eq-author/package.json +++ b/eq-author/package.json @@ -107,6 +107,7 @@ "draft-js-block-breakout-plugin": "latest", "draft-js-plugins-editor": "latest", "draft-js-raw-content-state": "latest", + "draft-js-import-html": "latest", "draftjs-filters": "^2.5.0", "firebase": "latest", "firebaseui": "latest", diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js index d64efb2ef0..481337291d 100644 --- a/eq-author/src/components/RichTextEditor/index.js +++ b/eq-author/src/components/RichTextEditor/index.js @@ -2,7 +2,14 @@ import React from "react"; import PropTypes from "prop-types"; import styled, { css } from "styled-components"; import Editor from "draft-js-plugins-editor"; -import { EditorState, RichUtils, Modifier, CompositeDecorator } from "draft-js"; +import { + EditorState, + RichUtils, + Modifier, + CompositeDecorator, + ContentState, +} from "draft-js"; +import { stateFromHTML } from "draft-js-import-html"; import "draft-js/dist/Draft.css"; import createBlockBreakoutPlugin from "draft-js-block-breakout-plugin"; @@ -395,12 +402,12 @@ class RichTextEditor extends React.Component { state = { showPasteModal: false, text: "", multiline: false }; - handlePaste = (text) => { + handlePaste = (text, html) => { if (/\s{2,}/g.test(text)) { this.setState({ showPasteModal: true, multiline: false, - text: text, + text: html || text, }); } else { this.handleChange( @@ -418,12 +425,12 @@ class RichTextEditor extends React.Component { return "handled"; }; - handlePasteMultiline = (text) => { + handlePasteMultiline = (text, html) => { if (/\s{2,}/g.test(text)) { this.setState({ showPasteModal: true, multiline: true, - text: text, + text: html || text, }); return "handled"; } else { @@ -436,27 +443,52 @@ class RichTextEditor extends React.Component { const currentContent = editorState.getCurrentContent(); const currentSelection = editorState.getSelection(); - let modifiedText; + let newEditorState; + let processedText = text; if (multiline) { - modifiedText = preserveRichFormatting(text); - } else { - modifiedText = text.replace(/\n/g, " ").trim().replace(/\s+/g, " "); - } + // Process the text to remove multiple spaces + processedText = processedText.replace(/\s{2,}/g, " ").trim(); + // Convert processed text from HTML to ContentState + const contentState = stateFromHTML(processedText); + const fragment = contentState.getBlockMap(); + + // Replace the selected text with the pasted content + const newContentState = Modifier.replaceWithFragment( + currentContent, + currentSelection, + fragment + ); - // Replace the selected text with the pasted content - const newContentState = Modifier.replaceText( - currentContent, - currentSelection, - modifiedText - ); + // Create a new EditorState with the updated content + newEditorState = EditorState.push( + editorState, + newContentState, + "insert-characters" + ); + } else { + // For single line pastes, replace multiple spaces with a single space + processedText = processedText + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); + const contentState = stateFromHTML(processedText); + const fragment = contentState.getBlockMap(); + + // Replace the selected text with the pasted content + const newContentState = Modifier.replaceWithFragment( + currentContent, + currentSelection, + fragment + ); - // Create a new EditorState with the updated content - const newEditorState = EditorState.push( - editorState, - newContentState, - "insert-characters" - ); + // Create a new EditorState with the updated content + newEditorState = EditorState.push( + editorState, + newContentState, + "insert-characters" + ); + } // Set the new editor state and close the paste modal this.setState({ diff --git a/eq-author/yarn.lock b/eq-author/yarn.lock index 7301bd4997..fabdcc8a4f 100644 --- a/eq-author/yarn.lock +++ b/eq-author/yarn.lock @@ -7331,6 +7331,21 @@ draft-js-block-breakout-plugin@latest: dependencies: immutable "~3.7.4" +draft-js-import-element@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz#8760acbfeb60ed824a1c8319ec049f702681df66" + integrity sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg== + dependencies: + draft-js-utils "^1.4.0" + synthetic-dom "^1.4.0" + +draft-js-import-html@latest: + version "1.4.1" + resolved "https://registry.yarnpkg.com/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz#c222a3a40ab27dee5874fcf78526b64734fe6ea4" + integrity sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg== + dependencies: + draft-js-import-element "^1.4.0" + draft-js-plugins-editor@latest: version "3.0.0" resolved "https://registry.yarnpkg.com/draft-js-plugins-editor/-/draft-js-plugins-editor-3.0.0.tgz#196d1e065e2c29faebaab4ec081b734fdef294a2" @@ -7346,6 +7361,11 @@ draft-js-raw-content-state@latest: dependencies: draft-js "^0.10.5" +draft-js-utils@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/draft-js-utils/-/draft-js-utils-1.4.1.tgz#a59c792ad621f7050292031a237d524708a6d509" + integrity sha512-xE81Y+z/muC5D5z9qWmKfxEW1XyXfsBzSbSBk2JRsoD0yzMGGHQm/0MtuqHl/EUDkaBJJLjJ2EACycoDMY/OOg== + draft-js@^0.10.5: version "0.10.5" resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742" @@ -16753,6 +16773,11 @@ synchronous-promise@latest: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e" integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== +synthetic-dom@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/synthetic-dom/-/synthetic-dom-1.4.0.tgz#d988d7a4652458e2fc8706a875417af913e4dd34" + integrity sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg== + tabbable@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.0.0.tgz#7f95ea69134e9335979092ba63866fe67b521b01" From 67062da1e7c74a7828e158d1e7203b55a132cb93 Mon Sep 17 00:00:00 2001 From: Paul-Joel Date: Wed, 22 May 2024 11:16:49 +0100 Subject: [PATCH 2/5] Remove redundent preserveRichFormatting --- .../src/components/RichTextEditor/index.js | 12 ++---------- .../src/components/modals/PasteModal/index.js | 13 ------------- .../components/modals/PasteModal/index.test.js | 18 +----------------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js index 481337291d..dc3795d5a5 100644 --- a/eq-author/src/components/RichTextEditor/index.js +++ b/eq-author/src/components/RichTextEditor/index.js @@ -2,13 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import styled, { css } from "styled-components"; import Editor from "draft-js-plugins-editor"; -import { - EditorState, - RichUtils, - Modifier, - CompositeDecorator, - ContentState, -} from "draft-js"; +import { EditorState, RichUtils, Modifier, CompositeDecorator } from "draft-js"; import { stateFromHTML } from "draft-js-import-html"; import "draft-js/dist/Draft.css"; import createBlockBreakoutPlugin from "draft-js-block-breakout-plugin"; @@ -34,9 +28,7 @@ import { sharedStyles } from "components/Forms/css"; import { Field, Label } from "components/Forms"; import ValidationError from "components/ValidationError"; -import PasteModal, { - preserveRichFormatting, -} from "components/modals/PasteModal"; +import PasteModal from "components/modals/PasteModal"; import { colors } from "../../constants/theme"; diff --git a/eq-author/src/components/modals/PasteModal/index.js b/eq-author/src/components/modals/PasteModal/index.js index 4e0ab19885..bb1ba1ab99 100644 --- a/eq-author/src/components/modals/PasteModal/index.js +++ b/eq-author/src/components/modals/PasteModal/index.js @@ -5,19 +5,6 @@ import PropTypes from "prop-types"; const Message = styled.div``; -export const preserveRichFormatting = (text) => { - // Replace multiple spaces and tabs with a single space - let formattedText = text.replace(/[ \t]+/g, " "); - - // Split the text into lines - let lines = formattedText.split(/\r?\n/); - - // Remove leading and trailing spaces from each line and join them back with newline characters - formattedText = lines.map((line) => line.trim()).join("\n"); - - return formattedText; -}; - const ModalWrapper = styled.div` .modal-button-container { margin-top: 1em; diff --git a/eq-author/src/components/modals/PasteModal/index.test.js b/eq-author/src/components/modals/PasteModal/index.test.js index 11f7631f15..1f3b70e8b5 100644 --- a/eq-author/src/components/modals/PasteModal/index.test.js +++ b/eq-author/src/components/modals/PasteModal/index.test.js @@ -1,27 +1,11 @@ import React from "react"; import { render, fireEvent } from "tests/utils/rtl"; -import PasteModal, { preserveRichFormatting } from "."; +import PasteModal from "."; import { keyCodes } from "constants/keyCodes"; const { Escape } = keyCodes; -describe("preserveRichFormatting function", () => { - it("should replace multiple spaces and tabs with a single space", () => { - const inputText = " Hello \t\t World "; - const expectedOutput = "Hello World"; - const result = preserveRichFormatting(inputText); - expect(result).toBe(expectedOutput); - }); - - it("should remove leading and trailing spaces from each line", () => { - const inputText = " Line 1 \n Line 2 \n Line 3 "; - const expectedOutput = "Line 1\nLine 2\nLine 3"; - const result = preserveRichFormatting(inputText); - expect(result).toBe(expectedOutput); - }); -}); - describe("PasteModal", () => { let onConfirm, onCancel; onConfirm = jest.fn(); From 3443deffdbd632bf81492069d6457c7027257d86 Mon Sep 17 00:00:00 2001 From: Paul-Joel Date: Fri, 31 May 2024 11:47:46 +0100 Subject: [PATCH 3/5] Add sanitize function --- .../src/components/RichTextEditor/index.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js index dc3795d5a5..7196cab542 100644 --- a/eq-author/src/components/RichTextEditor/index.js +++ b/eq-author/src/components/RichTextEditor/index.js @@ -438,9 +438,33 @@ class RichTextEditor extends React.Component { let newEditorState; let processedText = text; + // Simple HTML sanitization function + const sanitizeHtml = (html) => { + const doc = new DOMParser().parseFromString(html, "text/html"); + return doc.body.innerHTML; + }; + + // Sanitize the input HTML + const sanitizedHtml = sanitizeHtml(processedText); + if (multiline) { // Process the text to remove multiple spaces - processedText = processedText.replace(/\s{2,}/g, " ").trim(); + const div = document.createElement("div"); + div.innerHTML = sanitizedHtml; + const walker = document.createTreeWalker( + div, + NodeFilter.SHOW_TEXT, + null, + false + ); + while (walker.nextNode()) { + walker.currentNode.nodeValue = walker.currentNode.nodeValue.replace( + /\s{2,}/g, + " " + ); + } + processedText = div.innerHTML; + // Convert processed text from HTML to ContentState const contentState = stateFromHTML(processedText); const fragment = contentState.getBlockMap(); From ec453ec433be9406b1a4bbdfb1df620d523c54dd Mon Sep 17 00:00:00 2001 From: Paul-Joel Date: Fri, 31 May 2024 12:57:06 +0100 Subject: [PATCH 4/5] Tweak to ensure 2 spces are replaced from Word --- eq-author/src/components/RichTextEditor/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js index 7196cab542..793537ab11 100644 --- a/eq-author/src/components/RichTextEditor/index.js +++ b/eq-author/src/components/RichTextEditor/index.js @@ -458,10 +458,9 @@ class RichTextEditor extends React.Component { false ); while (walker.nextNode()) { - walker.currentNode.nodeValue = walker.currentNode.nodeValue.replace( - /\s{2,}/g, - " " - ); + walker.currentNode.nodeValue = walker.currentNode.nodeValue + .replace(/\s{2,}/g, " ") + .replace(/ /g, " "); } processedText = div.innerHTML; @@ -484,9 +483,10 @@ class RichTextEditor extends React.Component { ); } else { // For single line pastes, replace multiple spaces with a single space - processedText = processedText + processedText = sanitizedHtml .replace(/\n/g, " ") .replace(/\s+/g, " ") + .replace(/ /g, " ") .trim(); const contentState = stateFromHTML(processedText); const fragment = contentState.getBlockMap(); From fa5d2f28a0438e013be035de477049fd4dccadd6 Mon Sep 17 00:00:00 2001 From: Paul-Joel Date: Mon, 3 Jun 2024 10:29:47 +0100 Subject: [PATCH 5/5] Update to s+ --- eq-author/src/components/RichTextEditor/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eq-author/src/components/RichTextEditor/index.js b/eq-author/src/components/RichTextEditor/index.js index 793537ab11..ff62438798 100644 --- a/eq-author/src/components/RichTextEditor/index.js +++ b/eq-author/src/components/RichTextEditor/index.js @@ -458,9 +458,10 @@ class RichTextEditor extends React.Component { false ); while (walker.nextNode()) { - walker.currentNode.nodeValue = walker.currentNode.nodeValue - .replace(/\s{2,}/g, " ") - .replace(/ /g, " "); + walker.currentNode.nodeValue = walker.currentNode.nodeValue.replace( + /\s+/g, + " " + ); } processedText = div.innerHTML; @@ -483,10 +484,9 @@ class RichTextEditor extends React.Component { ); } else { // For single line pastes, replace multiple spaces with a single space - processedText = sanitizedHtml + processedText = processedText .replace(/\n/g, " ") .replace(/\s+/g, " ") - .replace(/ /g, " ") .trim(); const contentState = stateFromHTML(processedText); const fragment = contentState.getBlockMap();