diff --git a/package-lock.json b/package-lock.json index 2d76164adcd..e7513a573e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "redux": "^4.2.1", "redux-thunk": "^2.4.2", "rehype-raw": "^6.1.1", + "rehype-sanitize": "^6.0.0", "use-keyboard-shortcut": "^1.1.6", "xlsx": "^0.18.5" }, @@ -7069,8 +7070,7 @@ "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", - "dev": true + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", @@ -7538,8 +7538,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.6.0", @@ -13136,6 +13135,40 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-sanitize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz", + "integrity": "sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.2.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-sanitize/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-sanitize/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-parse5": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", @@ -20361,6 +20394,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sanitize/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-slug": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", diff --git a/package.json b/package.json index 5ce33f5c7a0..d85136e4b1f 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "redux": "^4.2.1", "redux-thunk": "^2.4.2", "rehype-raw": "^6.1.1", + "rehype-sanitize": "^6.0.0", "use-keyboard-shortcut": "^1.1.6", "xlsx": "^0.18.5" }, diff --git a/src/Components/Common/RichTextEditor/MarkdownPreview.tsx b/src/Components/Common/RichTextEditor/MarkdownPreview.tsx index c541eb6a8ca..549b29e15f5 100644 --- a/src/Components/Common/RichTextEditor/MarkdownPreview.tsx +++ b/src/Components/Common/RichTextEditor/MarkdownPreview.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import { UserBareMinimum } from "../../Users/models"; +import rehypeSanitize from "rehype-sanitize"; interface CustomLinkProps { className?: string; @@ -89,7 +90,7 @@ const MarkdownPreview = ({ return ( = ({ const selection = text.substring(start, end); const afterSelection = text.substring(end); - const newText = `${beforeSelection}${prefix}${selection}${suffix}${afterSelection}`; + let newText = ""; + let newCursorPosition = 0; + + if (selection) { + newText = `${beforeSelection}${prefix}${selection}${suffix}${afterSelection}`; + newCursorPosition = start + prefix.length + selection.length; + } else { + newText = `${beforeSelection}${prefix}${suffix}${afterSelection}`; + newCursorPosition = start + prefix.length; + } setMarkdown(newText); - editorRef.current.focus(); - editorRef.current.setSelectionRange( - start + prefix.length, - end + prefix.length, - ); + + // Using setTimeout to ensure the new text is set before we try to move the cursor + setTimeout(() => { + if (editorRef.current) { + editorRef.current.focus(); + editorRef.current.setSelectionRange( + newCursorPosition, + newCursorPosition, + ); + } + }, 0); }; + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setShowMentions(false); + setMentionFilter(""); + } + }; + + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, []); + const handleInput = useCallback( (event: React.ChangeEvent) => { const newMarkdown = event.target.value; @@ -78,17 +113,16 @@ const RichTextEditor: React.FC = ({ if (lastAtSymbolIndex !== -1) { const mentionText = textBeforeCaret.substring(lastAtSymbolIndex + 1); - if (mentionText.trim() !== "") { - setMentionFilter(mentionText); - - if (editorRef.current) { - const { top, left } = getCaretCoordinates( - editorRef.current, - caretPosition, - ); - setMentionPosition({ top: top + 40, left }); - setShowMentions(true); - } + if (mentionText.includes(" ")) return; + setMentionFilter(mentionText); + + if (editorRef.current) { + const { top, left } = getCaretCoordinates( + editorRef.current, + caretPosition, + ); + setMentionPosition({ top: top + 50, left: left + 10 }); + setShowMentions(true); } } else { setShowMentions(false); @@ -104,14 +138,17 @@ const RichTextEditor: React.FC = ({ const currentLine = lines[lines.length - 1]; if (/^\d+\.\s/.test(currentLine)) { - handleOrderedList(); e.preventDefault(); + e.stopPropagation(); + handleOrderedList(); } else if (/^-\s/.test(currentLine)) { - handleUnorderedList(); e.preventDefault(); + e.stopPropagation(); + handleUnorderedList(); } else if (/^>\s/.test(currentLine)) { - handleQuote(); e.preventDefault(); + e.stopPropagation(); + handleQuote(); } } }; @@ -123,8 +160,6 @@ const RichTextEditor: React.FC = ({ const text = editorRef.current.value; const lastAtSymbolIndex = text.lastIndexOf("@", start - 1); - if (lastAtSymbolIndex === -1) return; - const beforeMention = text.substring(0, lastAtSymbolIndex); const afterMention = text.substring(start); @@ -137,6 +172,7 @@ const RichTextEditor: React.FC = ({ editorRef.current.setSelectionRange(newCursorPosition, newCursorPosition); setShowMentions(false); + setMentionFilter(""); }; const handleOrderedList = () => { diff --git a/src/Components/Facility/ConsultationDoctorNotes/index.tsx b/src/Components/Facility/ConsultationDoctorNotes/index.tsx index d570100faec..6340ecd837e 100644 --- a/src/Components/Facility/ConsultationDoctorNotes/index.tsx +++ b/src/Components/Facility/ConsultationDoctorNotes/index.tsx @@ -43,7 +43,6 @@ const ConsultationDoctorNotes = (props: ConsultationDoctorNotesProps) => { "default-view", ); const [threadViewNote, setThreadViewNote] = useState(""); - console.log("ConsultationDoctorNotes -> threadViewNote", threadViewNote); const initialData: PatientNoteStateType = { notes: [],