11import CONST from '@src/CONST' ;
22
3- import type { ChildNode } from 'domhandler' ;
3+ import type { ChildNode , ParentNode } from 'domhandler' ;
44
55import render from 'dom-serializer' ;
66import { DataNode , Element } from 'domhandler' ;
@@ -12,6 +12,35 @@ import type GetCurrentSelection from './types';
1212const markdownElements = new Set ( [ 'h1' , 'strong' , 'em' , 'del' , 'blockquote' , 'q' , 'code' , 'pre' , 'a' , 'br' , 'li' , 'ul' , 'ol' , 'b' , 'i' , 's' , 'mention-user' ] ) ;
1313const tagAttribute = 'data-testid' ;
1414
15+ const installTransformedChildren = ( parent : ParentNode , children : ChildNode [ ] ) => {
16+ const transformedParent = parent ;
17+ transformedParent . children = children ;
18+
19+ for ( const [ index , child ] of children . entries ( ) ) {
20+ child . parent = transformedParent ;
21+ child . prev = index > 0 ? ( children . at ( index - 1 ) ?? null ) : null ;
22+ child . next = children . at ( index + 1 ) ?? null ;
23+ }
24+ } ;
25+
26+ const installForeignObjectRenderCompatibility = ( parent : ParentNode , isWithinSVG = false ) => {
27+ for ( const child of parent . children ) {
28+ if ( ! ( child instanceof Element ) ) {
29+ continue ;
30+ }
31+
32+ const childIsWithinSVG = isWithinSVG || child . name === 'svg' ;
33+ if ( childIsWithinSVG && child . name === 'foreignobject' ) {
34+ const lowercaseForeignObject = child . cloneNode ( ) ;
35+ for ( const foreignObjectChild of child . children ) {
36+ foreignObjectChild . parent = lowercaseForeignObject ;
37+ }
38+ }
39+
40+ installForeignObjectRenderCompatibility ( child , childIsWithinSVG ) ;
41+ }
42+ } ;
43+
1544/**
1645 * Reads html of selection. If browser doesn't support Selection API, returns empty string.
1746 * @returns HTML of selection as String
@@ -53,7 +82,7 @@ const getHTMLOfSelection = (): string => {
5382 // If clonedSelection has no text content this data has no meaning to us.
5483 if ( clonedSelection . textContent ) {
5584 let parent : globalThis . Element | null = null ;
56- let child = clonedSelection ;
85+ let child : globalThis . Node = clonedSelection ;
5786
5887 // If selection starts and ends within same text node we use its parentNode. This is because we can't
5988 // use closest function on a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node.
@@ -73,16 +102,16 @@ const getHTMLOfSelection = (): string => {
73102 if ( range . commonAncestorContainer instanceof HTMLElement ) {
74103 parent = range . commonAncestorContainer . closest ( `[${ tagAttribute } ]` ) ;
75104 } else {
76- parent = ( range . commonAncestorContainer . parentNode as HTMLElement | null ) ?. closest ( `[${ tagAttribute } ]` ) ?? null ;
105+ parent = range . commonAncestorContainer . parentElement ?. closest ( `[${ tagAttribute } ]` ) ?? null ;
77106 }
78107
79108 // Keep traversing up to clone all parents with 'data-testid' attribute.
80109 while ( parent ) {
81110 const cloned = parent . cloneNode ( ) ;
82111 cloned . appendChild ( child ) ;
83- child = cloned as DocumentFragment ;
112+ child = cloned ;
84113
85- parent = ( parent . parentNode as HTMLElement | null ) ?. closest ( `[${ tagAttribute } ]` ) ?? null ;
114+ parent = parent . parentElement ?. closest ( `[${ tagAttribute } ]` ) ?? null ;
86115 }
87116
88117 div . appendChild ( child ) ;
@@ -107,24 +136,25 @@ const getHTMLOfSelection = (): string => {
107136 * @param dom - dom htmlparser2 dom representation
108137 */
109138const replaceNodes = ( dom : ChildNode , isChildOfEditorElement : boolean ) : ChildNode => {
110- let domName ;
111- let domChildren : ChildNode [ ] = [ ] ;
112- const domAttribs : Element [ 'attribs' ] = { } ;
113- let data = '' ;
114-
115139 // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method.
116140 if ( dom . type . toString ( ) === 'text' && dom instanceof DataNode ) {
117- data = Str . htmlEncode ( dom . data ) ;
141+ const clonedDom = dom . cloneNode ( ) ;
142+ clonedDom . data = Str . htmlEncode ( dom . data ) ;
118143 if ( dom . parent instanceof Element && dom . parent ?. attribs ?. [ tagAttribute ] === 'email-with-break-opportunities' ) {
119- data = data . replaceAll ( '\u200b' , '' ) ;
144+ clonedDom . data = clonedDom . data . replaceAll ( '\u200b' , '' ) ;
120145 }
121- } else if ( dom instanceof Element ) {
122- domName = dom . name ;
146+
147+ return clonedDom ;
148+ }
149+
150+ if ( dom instanceof Element ) {
151+ const clonedDom = dom . cloneNode ( ) ;
152+ clonedDom . attribs = { } ;
123153 const child = dom . children . at ( 0 ) ;
124154 if ( dom . attribs ?. [ tagAttribute ] ) {
125155 // If it's a markdown element, rename it according to the value of data-testid, so ExpensiMark can parse it
126156 if ( markdownElements . has ( dom . attribs [ tagAttribute ] ) ) {
127- domName = dom . attribs [ tagAttribute ] ;
157+ clonedDom . name = dom . attribs [ tagAttribute ] ;
128158 }
129159 } else if ( dom . name === 'div' && dom . children . length === 1 && isChildOfEditorElement && child ) {
130160 // We are excluding divs that are children of our editor element and have only one child to prevent
@@ -134,36 +164,35 @@ const replaceNodes = (dom: ChildNode, isChildOfEditorElement: boolean): ChildNod
134164
135165 // We need to preserve href attribute in order to copy links.
136166 if ( dom . attribs ?. href ) {
137- domAttribs . href = dom . attribs . href ;
167+ clonedDom . attribs . href = dom . attribs . href ;
138168 }
139169
140- if ( dom . children ) {
141- domChildren = dom . children . map ( ( c ) => replaceNodes ( c , isChildOfEditorElement || ! ! dom . attribs ?. [ tagAttribute ] ) ) ;
142- }
143- } else {
144- throw new Error ( `Unknown dom type: ${ dom . type } ` ) ;
170+ const transformedChildren = dom . children . map ( ( c ) => replaceNodes ( c , isChildOfEditorElement || ! ! dom . attribs ?. [ tagAttribute ] ) ) ;
171+ installTransformedChildren ( clonedDom , transformedChildren ) ;
172+ return clonedDom ;
145173 }
146174
147- return {
148- ...dom ,
149- data,
150- name : domName ,
151- attribs : domAttribs ,
152- children : domChildren ,
153- } as Element & DataNode ;
175+ throw new Error ( `Unknown dom type: ${ dom . type } ` ) ;
154176} ;
155177
156178/**
157179 * Resolves the current selection to values and produces clean HTML.
158180 */
159181const getCurrentSelection : GetCurrentSelection = ( ) => {
160- const domRepresentation = parseDocument ( getHTMLOfSelection ( ) ) ;
161- domRepresentation . children = domRepresentation . children . map ( ( item ) => replaceNodes ( item , false ) ) ;
182+ const parsedDom = parseDocument ( getHTMLOfSelection ( ) ) ;
183+ const domRepresentation = parsedDom . cloneNode ( ) ;
184+ installTransformedChildren (
185+ domRepresentation ,
186+ parsedDom . children . map ( ( item ) => replaceNodes ( item , false ) ) ,
187+ ) ;
188+
189+ const renderView = domRepresentation . cloneNode ( true ) ;
190+ installForeignObjectRenderCompatibility ( renderView ) ;
162191
163192 // Newline characters need to be removed here because the HTML could contain both newlines and <br> tags, and when
164193 // <br> tags are converted later to markdown, it creates duplicate newline characters. This means that when the content
165194 // is pasted, there are extra newlines in the content that we want to avoid.
166- const newHtml = render ( domRepresentation ) . replaceAll ( '<br>\n' , '<br>' ) ;
195+ const newHtml = render ( renderView ) . replaceAll ( '<br>\n' , '<br>' ) ;
167196 return newHtml || '' ;
168197} ;
169198
0 commit comments