@@ -4,112 +4,94 @@ define(function (require, exports, module) {
44 const CodeMirror = require ( "thirdparty/CodeMirror/lib/codemirror" ) ;
55
66 /**
7- * This function is to sync text content changes between the original source code
8- * and the live preview DOM after a text edit operation
7+ * This function syncs text content changes between the original source code
8+ * and the live preview DOM after a text edit in the browser
99 *
10+ * @private
1011 * @param {String } oldContent - the original source code from the editor
11- * @param {String } newContent - the DOM element's outerHTML after editing in live preview
12- * @returns {String } - the updated content that should replace the original code in the editor
12+ * @param {String } newContent - the outerHTML after editing in live preview
13+ * @returns {String } - the updated content that should replace the original editor code
1314 *
14- * NOTE: This function is a bit complex to read, read this jsdoc to understand the flow:
15+ * NOTE: We don’t touch tag names or attributes —
16+ * we only care about text changes or things like newlines, <br>, or formatting like <b>, <i>, etc.
1517 *
16- * First, we parse both the old and new content using DOMParser to get proper HTML DOM structures
17- * Then we compare each element and text node between the old and new content
18+ * Here's the basic idea:
19+ * - Parse both old and new HTML strings into DOM trees
20+ * - Then walk both DOMs side by side and sync changes
1821 *
19- * the main goal is that we ONLY want to update text content, and not element nodes or their attributes
20- * because if we allow element/attribute changes, the browser might try to fix the HTML
21- * to make it syntactically correct or to make it efficient, which would mess up the user's original code
22- * We don't want that - we need to respect how the user wrote their code
23- * For example: if user wrote <div style="color: red blue green yellow"></div>
24- * The browser sees this is invalid CSS and would remove the color attribute entirely
25- * We want to keep that invalid code as it is because it's what the user wanted to do
22+ * What we handle:
23+ * - if both are text nodes → update the text if changed
24+ * - if both are elements with same tag → go deeper and sync their children
25+ * - if one is text and one is an element → replace (like when user adds/removes <br> or adds bold/italic)
26+ * - if a node got added or removed → do that in the old DOM
2627 *
27- * Here's how the comparison works:
28- * - if both nodes are text: update the old text with the new text
29- * - if both nodes are elements: we recursively check their children (for nested content)
30- * - if old is text, new is element: replace text with element (like when user adds <br>)
31- * - if old is element, new is text: replace element with text (like when user removes <br>)
32- * note: when adding new elements (like <br> tags), we only copy the tag name and content,
33- * never the attributes, to avoid internal Phoenix properties leaking into user's code
28+ * We don’t recreate or touch existing elements unless absolutely needed,
29+ * so all original user-written attributes and tag structure stay exactly the same.
30+ *
31+ * This avoids the browser trying to “fix” broken HTML (which we don’t want)
3432 */
3533 function _syncTextContentChanges ( oldContent , newContent ) {
3634 const parser = new DOMParser ( ) ;
3735 const oldDoc = parser . parseFromString ( oldContent , "text/html" ) ;
3836 const newDoc = parser . parseFromString ( newContent , "text/html" ) ;
3937
40- // as DOM parser will add the complete html structure with the HTML tags and all,
41- // so we just need to get the main content
4238 const oldRoot = oldDoc . body ;
4339 const newRoot = newDoc . body ;
4440
45- // here oldNode and newNode are full HTML elements which are direct children of the body tag
4641 function syncText ( oldNode , newNode ) {
4742 if ( ! oldNode || ! newNode ) {
4843 return ;
4944 }
5045
51- // if both the oldNode and newNode has text, replace the old node's text content with the new one
46+ // when both are text nodes, we just need to replace the old text with the new one
5247 if ( oldNode . nodeType === Node . TEXT_NODE && newNode . nodeType === Node . TEXT_NODE ) {
53- oldNode . nodeValue = newNode . nodeValue ;
54-
55- } else if (
56- // if both have element node, then we recursively get their child elements
57- // this is so that we can get & update the text content in deeply nested DOM
58- oldNode . nodeType === Node . ELEMENT_NODE &&
59- newNode . nodeType === Node . ELEMENT_NODE
60- ) {
48+ if ( oldNode . nodeValue !== newNode . nodeValue ) {
49+ oldNode . nodeValue = newNode . nodeValue ;
50+ }
51+ return ;
52+ }
6153
62- const oldChildren = oldNode . childNodes ;
63- const newChildren = newNode . childNodes ;
54+ // when both are elements
55+ if ( oldNode . nodeType === Node . ELEMENT_NODE && newNode . nodeType === Node . ELEMENT_NODE ) {
56+ const oldChildren = Array . from ( oldNode . childNodes ) ;
57+ const newChildren = Array . from ( newNode . childNodes ) ;
6458
65- const minLength = Math . min ( oldChildren . length , newChildren . length ) ;
59+ const maxLen = Math . max ( oldChildren . length , newChildren . length ) ;
6660
67- for ( let i = 0 ; i < minLength ; i ++ ) {
68- syncText ( oldChildren [ i ] , newChildren [ i ] ) ;
69- }
70-
71- // append if there are any new nodes, this is mainly when <br> tags needs to be inserted
72- // as user pressed shift + enter to create empty lines in the new content
73- for ( let i = minLength ; i < newChildren . length ; i ++ ) {
61+ for ( let i = 0 ; i < maxLen ; i ++ ) {
62+ const oldChild = oldChildren [ i ] ;
7463 const newChild = newChildren [ i ] ;
75- let cleanChild ;
7664
77- if ( newChild . nodeType === Node . ELEMENT_NODE ) {
78- // only the element name and not its attributes
79- // this is to prevent internal properties like data-brackets-id, etc to appear in users code
80- cleanChild = document . createElement ( newChild . tagName ) ;
81- cleanChild . innerHTML = newChild . innerHTML ;
65+ if ( ! oldChild && newChild ) {
66+ // if new child added → clone and insert
67+ oldNode . appendChild ( newChild . cloneNode ( true ) ) ;
68+ } else if ( oldChild && ! newChild ) {
69+ // if child removed → delete
70+ oldNode . removeChild ( oldChild ) ;
71+ } else if (
72+ oldChild . nodeType === newChild . nodeType &&
73+ oldChild . nodeType === Node . ELEMENT_NODE &&
74+ oldChild . tagName === newChild . tagName
75+ ) {
76+ // same element tag → sync recursively
77+ syncText ( oldChild , newChild ) ;
78+ } else if (
79+ oldChild . nodeType === Node . TEXT_NODE &&
80+ newChild . nodeType === Node . TEXT_NODE
81+ ) {
82+ if ( oldChild . nodeValue !== newChild . nodeValue ) {
83+ oldChild . nodeValue = newChild . nodeValue ;
84+ }
8285 } else {
83- // for text nodes, comment nodes, etc. clone normally
84- cleanChild = newChild . cloneNode ( true ) ;
86+ // different node types or tags → replace
87+ oldNode . replaceChild ( newChild . cloneNode ( true ) , oldChild ) ;
8588 }
86-
87- oldNode . appendChild ( cleanChild ) ;
8889 }
89-
90- // remove extra old nodes (maybe extra <br>'s were removed)
91- for ( let i = oldChildren . length - 1 ; i >= newChildren . length ; i -- ) {
92- oldNode . removeChild ( oldChildren [ i ] ) ;
93- }
94-
95- } else if ( oldNode . nodeType === Node . TEXT_NODE && newNode . nodeType === Node . ELEMENT_NODE ) {
96- // when old has text node and new has element node
97- // this generally happens when we remove the complete content which results in empty <br> tag
98- // for ex: <div>hello</div>, here if we remove the 'hello' from live preview then result will be
99- // <div><br></div>
100- const replacement = document . createElement ( newNode . tagName ) ;
101- replacement . innerHTML = newNode . innerHTML ;
102- oldNode . parentNode . replaceChild ( replacement , oldNode ) ;
103- } else if ( oldNode . nodeType === Node . ELEMENT_NODE && newNode . nodeType === Node . TEXT_NODE ) {
104- // this is opposite of previous one when earlier it was just <br> or some tag
105- // and now we add text content in that
106- const replacement = document . createTextNode ( newNode . nodeValue ) ;
107- oldNode . parentNode . replaceChild ( replacement , oldNode ) ;
10890 }
10991 }
11092
111- const oldEls = oldRoot . children ;
112- const newEls = newRoot . children ;
93+ const oldEls = Array . from ( oldRoot . children ) ;
94+ const newEls = Array . from ( newRoot . children ) ;
11395
11496 for ( let i = 0 ; i < Math . min ( oldEls . length , newEls . length ) ; i ++ ) {
11597 syncText ( oldEls [ i ] , newEls [ i ] ) ;
0 commit comments