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:
- "
- Data should relate to all sites in England, Scotland, Wales and Northern Ireland unless otherwise stated.
- You can provide info estimates if actual figures are not available.
- We will treat your data securely and confidentially.
",
+ "- Data should relate to all sites in England, Scotland, Wales and Northern Ireland unless otherwise stated.
- You can provide info estimates if actual figures are not available.
- We will treat your data securely and confidentially.
",
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..ff62438798 100644
--- a/eq-author/src/components/RichTextEditor/index.js
+++ b/eq-author/src/components/RichTextEditor/index.js
@@ -3,6 +3,7 @@ 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 { stateFromHTML } from "draft-js-import-html";
import "draft-js/dist/Draft.css";
import createBlockBreakoutPlugin from "draft-js-block-breakout-plugin";
@@ -27,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";
@@ -395,12 +394,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 +417,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 +435,76 @@ class RichTextEditor extends React.Component {
const currentContent = editorState.getCurrentContent();
const currentSelection = editorState.getSelection();
- let modifiedText;
+ 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) {
- modifiedText = preserveRichFormatting(text);
- } else {
- modifiedText = text.replace(/\n/g, " ").trim().replace(/\s+/g, " ");
- }
+ // Process the text to remove multiple spaces
+ 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+/g,
+ " "
+ );
+ }
+ processedText = div.innerHTML;
- // Replace the selected text with the pasted content
- const newContentState = Modifier.replaceText(
- currentContent,
- currentSelection,
- modifiedText
- );
+ // Convert processed text from HTML to ContentState
+ const contentState = stateFromHTML(processedText);
+ const fragment = contentState.getBlockMap();
- // Create a new EditorState with the updated content
- const newEditorState = EditorState.push(
- editorState,
- newContentState,
- "insert-characters"
- );
+ // Replace the selected text with the pasted content
+ const newContentState = Modifier.replaceWithFragment(
+ currentContent,
+ currentSelection,
+ fragment
+ );
+
+ // 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
+ newEditorState = EditorState.push(
+ editorState,
+ newContentState,
+ "insert-characters"
+ );
+ }
// Set the new editor state and close the paste modal
this.setState({
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();
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"