Skip to content

Commit 1ae042b

Browse files
committed
fix: text formatting not working when trying to edit text in live preview
1 parent 24c1ca0 commit 1ae042b

File tree

1 file changed

+56
-74
lines changed

1 file changed

+56
-74
lines changed

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 56 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)